mirror of
https://github.com/google-github-actions/auth.git
synced 2026-06-07 06:17:05 +00:00
Compare commits
251 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 |
||
|
|
c4799db911 |
||
|
|
deb4ef6f17 |
||
|
|
2671692c37 |
||
|
|
5ed7985d75 |
||
|
|
dac4e13deb |
||
|
|
c4e906d161 |
||
|
|
246692c248 |
||
|
|
c06176fb62 |
||
|
|
2b207063da |
||
|
|
acd8a2b487 |
||
|
|
8609d2cff4 |
||
|
|
d394832e06 |
||
|
|
b792fe33f8 |
||
|
|
02b3962042 |
||
|
|
157280cef2 |
||
|
|
a771ff81e2 |
||
|
|
d786187a9e |
||
|
|
1336d3b920 |
||
|
|
7c65c4311a |
||
|
|
a1109f2c8e |
||
|
|
f899132198 |
||
|
|
773e856e31 |
||
|
|
d4641feafe |
||
|
|
ceee102ec2 |
||
|
|
b60aafc09d |
||
|
|
9025e64312 |
||
|
|
30b1c78d51 |
||
|
|
95a6bc2a27 |
||
|
|
10d8e00a99 |
||
|
|
81012c2689 |
||
|
|
7c32666372 |
||
|
|
714f1fe243 |
||
|
|
dafc92490a |
||
|
|
cf96743e5a |
||
|
|
a1f1aca0d6 |
||
|
|
f322a0d1e2 |
||
|
|
ce2cf1ac23 |
||
|
|
b258a9f230 |
||
|
|
e2458b7f8a |
||
|
|
b5b6d978c9 |
||
|
|
c2d3c1fca0 |
||
|
|
f6793d5041 |
||
|
|
9adccd9156 |
||
|
|
b05f71482f |
||
|
|
50dbfd0907 |
||
|
|
e426d954f6 |
||
|
|
d16fd896f7 |
||
|
|
38d3c2f54d |
||
|
|
e9ca24f3e2 |
||
|
|
db6919d074 |
||
|
|
8d936a3c13 |
||
|
|
65963dbda5 |
||
|
|
3fbe03175e |
||
|
|
a2b753f630 |
||
|
|
8d125895b9 |
||
|
|
696f31dbf6 |
||
|
|
f9dc3d62d1 |
||
|
|
48c46e6a59 |
||
|
|
3b7fb59565 |
||
|
|
b6d69ec4d4 |
||
|
|
5d9db97c33 |
||
|
|
736e11de40 |
||
|
|
983a037dfb |
||
|
|
ac489d50bb |
||
|
|
7a360a247c |
||
|
|
7b354992e5 |
||
|
|
88fbfac1f6 |
||
|
|
54924dbbed |
||
|
|
1261433f2b |
||
|
|
2c3f0ad713 |
||
|
|
f616570cea |
||
|
|
9b6531516d |
||
|
|
478002e00b |
||
|
|
419a2c3bfe |
||
|
|
443ae925ab |
||
|
|
8c15757ad6 |
||
|
|
c6c22902f6 |
||
|
|
0aa6edff98 |
||
|
|
c2df8017cd |
||
|
|
b39ebee9c1 |
||
|
|
ec59fe8c8e |
||
|
|
5975d7549d |
||
|
|
1b8ec4e237 |
||
|
|
096ab1fb9a |
||
|
|
1be7cd0ce2 |
||
|
|
52d3869752 |
||
|
|
1c6c798434 |
||
|
|
ac09c292a1 |
||
|
|
d03480e8ad |
||
|
|
5090ecb28d |
||
|
|
ccc7806970 |
||
|
|
1618f1c032 |
||
|
|
870ff908cc |
||
|
|
ed829381c8 |
||
|
|
abba7c4d9a |
||
|
|
8708e498da |
||
|
|
057960bb62 |
||
|
|
1e9245c68a |
||
|
|
c6fa692def |
||
|
|
a37d7b422e |
||
|
|
6dbf4e537f |
||
|
|
e805d1d054 |
||
|
|
9a051ab76c |
||
|
|
2428105372 |
||
|
|
ddbc4798b7 |
||
|
|
fc5003a59a |
||
|
|
e7aec2b7c8 |
||
|
|
cc4b368646 |
||
|
|
c022bd29f8 |
||
|
|
3ecfcdb218 |
||
|
|
6c3096cce2 |
||
|
|
f56f5989ce |
||
|
|
19e4d7845e |
||
|
|
a61909d048 |
||
|
|
b759bed608 |
||
|
|
f014f2369a |
||
|
|
d5ed5bab89 |
||
|
|
d97e31546b |
||
|
|
2f0b4dbd9b |
||
|
|
d5a354ef10 |
||
|
|
3fe2a3779a |
||
|
|
90e80b8d07 |
||
|
|
abe0e87145 |
||
|
|
f3c3e206c9 |
||
|
|
02f3d58995 |
||
|
|
60904d8a0d |
||
|
|
b3b82c7f1e |
||
|
|
7296e5030f |
||
|
|
f8bb88e07e |
||
|
|
0583f8fbeb |
||
|
|
fe9d1eddf5 |
||
|
|
1854238d2d |
||
|
|
5f24906e9b |
||
|
|
f660f85abd |
||
|
|
fb26879445 |
||
|
|
2dd133ffa2 |
||
|
|
febe21311b |
||
|
|
c7bb6ad28f |
||
|
|
bbbd1424f7 |
||
|
|
03500c2f23 |
||
|
|
ddb73aed67 |
||
|
|
ad2dddb569 |
||
|
|
5e33466111 |
||
|
|
cb396c3f31 |
||
|
|
afef6a5b6d |
||
|
|
e13dfdd573 |
||
|
|
271e0346a0 |
41 changed files with 4976 additions and 4845 deletions
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"'
|
||||
24
.github/dependabot.yml
vendored
Normal file
24
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# 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: '/'
|
||||
rebase-strategy: 'disabled'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
commit-message:
|
||||
prefix: 'security: '
|
||||
open-pull-requests-limit: 0 # only check security updates
|
||||
25
.github/workflows/draft-release.yml
vendored
Normal file
25
.github/workflows/draft-release.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: 'Draft release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_strategy:
|
||||
description: 'Version strategy: The strategy to used to update the version based on semantic versioning (more info at https://semver.org/).'
|
||||
required: true
|
||||
default: 'patch'
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'major'
|
||||
- 'minor'
|
||||
- 'patch'
|
||||
|
||||
jobs:
|
||||
draft-release:
|
||||
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:
|
||||
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 }}'
|
||||
17
.github/workflows/release.yml
vendored
Normal file
17
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
name: 'Release'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**/*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
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 }}'
|
||||
45
.github/workflows/test.yaml
vendored
45
.github/workflows/test.yaml
vendored
|
|
@ -1,45 +0,0 @@
|
|||
name: 'test'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
run:
|
||||
name: 'test'
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
runs-on: '${{ matrix.operating-system }}'
|
||||
strategy:
|
||||
matrix:
|
||||
operating-system:
|
||||
- 'ubuntu-latest'
|
||||
- 'windows-latest'
|
||||
- 'macos-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
|
||||
- uses: 'actions/setup-node@master'
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- id: 'integration'
|
||||
name: 'integration'
|
||||
uses: './'
|
||||
with:
|
||||
workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/github-oidc-auth-google-cloud'
|
||||
service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com'
|
||||
|
||||
- name: 'npm install'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'npm lint'
|
||||
run: 'npm run lint'
|
||||
|
||||
- name: 'npm test'
|
||||
run: 'npm run test'
|
||||
282
.github/workflows/test.yml
vendored
Normal file
282
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# 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@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'
|
||||
|
||||
- name: 'npm test'
|
||||
run: 'npm run test'
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
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:
|
||||
project_id: '${{ vars.PROJECT_ID }}'
|
||||
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
|
||||
|
||||
- id: 'oauth-federated-token'
|
||||
name: 'oauth-federated-token'
|
||||
run: |-
|
||||
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: '${{ 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'
|
||||
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: 'auth-id-token'
|
||||
name: 'auth-id-token'
|
||||
uses: './'
|
||||
with:
|
||||
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
|
||||
token_format: 'id_token'
|
||||
id_token_audience: 'https://secretmanager.googleapis.com/'
|
||||
id_token_include_email: true
|
||||
|
||||
#
|
||||
# This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment
|
||||
# variable is shared with the container and that the path of the file is on
|
||||
# the shared filesystem with the container and that the USER for the container
|
||||
# has permissions to read the file.
|
||||
#
|
||||
docker:
|
||||
if: |-
|
||||
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
||||
name: 'docker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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'
|
||||
|
||||
- name: 'auth-default'
|
||||
uses: './'
|
||||
with:
|
||||
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
|
||||
|
||||
- name: 'docker'
|
||||
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}"'
|
||||
46
.github/workflows/troubleshooting.yml
vendored
Normal file
46
.github/workflows/troubleshooting.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# 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:
|
||||
issues:
|
||||
types:
|
||||
- 'opened'
|
||||
|
||||
jobs:
|
||||
troubleshooting:
|
||||
permissions:
|
||||
issues: 'write'
|
||||
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7
|
||||
with:
|
||||
script: |-
|
||||
const msg =
|
||||
`Hi there @${context.actor} :wave:!\n` +
|
||||
`\n` +
|
||||
`Thank you for opening an issue. Our team will triage this as soon as we ` +
|
||||
`can. Please take a moment to review the ` +
|
||||
`[troubleshooting steps](https://github.com/google-github-actions/auth/blob/main/docs/TROUBLESHOOTING.md) ` +
|
||||
`which lists common error messages and their resolution steps.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: msg,
|
||||
});
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,7 +1,7 @@
|
|||
node_modules/
|
||||
runner/
|
||||
|
||||
# Rest of the file pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
|
||||
# Rest of the file pulled from https://github.com/github/gitignore/blob/main/Node.gitignore
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ module.exports = {
|
|||
arrowParens: 'always',
|
||||
bracketSpacing: true,
|
||||
endOfLine: 'auto',
|
||||
jsxBracketSameLine: true,
|
||||
jsxSingleQuote: true,
|
||||
printWidth: 100,
|
||||
quoteProps: 'consistent',
|
||||
|
|
|
|||
4
CHANGELOG.md
Normal file
4
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Changelog
|
||||
|
||||
Changelogs for each release are located on the [releases page](https://github.com/google-github-actions/auth/releases).
|
||||
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
|
|
@ -0,0 +1 @@
|
|||
* @google-github-actions/maintainers
|
||||
691
README.md
691
README.md
|
|
@ -1,201 +1,682 @@
|
|||
# oidc-auth-google-cloud
|
||||
# Authenticate to Google Cloud from GitHub Actions
|
||||
|
||||
This GitHub Action exchanges a GitHub Actions OIDC token into a Google Cloud
|
||||
access token using [Workload Identity Federation][wif]. This obviates the need
|
||||
to export a long-lived Google Cloud service account key and establishes a trust
|
||||
This GitHub Action authenticates to Google Cloud. It supports authentication via
|
||||
a Google Cloud Service Account Key JSON and authentication via [Workload
|
||||
Identity Federation][wif].
|
||||
|
||||
Workload Identity Federation is recommended over Service Account Keys as it
|
||||
obviates the need to export a long-lived credential and establishes a trust
|
||||
delegation relationship between a particular GitHub Actions workflow invocation
|
||||
and permissions on Google Cloud.
|
||||
and permissions on Google Cloud. There are three ways to set up this GitHub
|
||||
Action to authenticate to Google Cloud:
|
||||
|
||||
#### Previously
|
||||
1. [(Preferred) Direct Workload Identity Federation](#direct-wif)
|
||||
1. [Workload Identity Federation through a Service Account](#indirect-wif)
|
||||
1. [Service Account Key JSON](#sake)
|
||||
|
||||
1. Create a Google Cloud service account and grant IAM permissions
|
||||
1. Export the long-lived JSON service account key
|
||||
1. Upload the JSON service account key to a GitHub secret
|
||||
> [!IMPORTANT]
|
||||
> The `gsutil` command will **not** use the credentials exported by this GitHub
|
||||
> Action. Customers should use `gcloud storage` instead.
|
||||
|
||||
#### With Workload Identity Federation
|
||||
**This is not an officially supported Google product, and it is not covered by a
|
||||
Google Cloud support contract. To report bugs or request features in a Google
|
||||
Cloud product, please contact [Google Cloud
|
||||
support](https://cloud.google.com/support).**
|
||||
|
||||
1. Create a Google Cloud service account and grant IAM permissions
|
||||
1. Create and configure a Workload Identity Provider for GitHub
|
||||
1. Exchange the GitHub Actions OIDC token for a short-lived Google Cloud access
|
||||
token
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- This action requires you to create and configure a Google Cloud Workload
|
||||
Identity Provider. See [#setup] for instructions.
|
||||
- Run the `actions/checkout@v4` step _before_ this action. Omitting the
|
||||
checkout step or putting it after `auth` will cause future steps to be
|
||||
unable to authenticate.
|
||||
|
||||
- To create binaries, containers, pull requests, or other releases, add the
|
||||
following to your `.gitignore`, `.dockerignore` and similar files to prevent
|
||||
accidentally committing credentials to your release artifact:
|
||||
|
||||
```text
|
||||
# Ignore generated credentials from google-github-actions/auth
|
||||
gha-creds-*.json
|
||||
```
|
||||
|
||||
- This action runs using Node 24. Use a [runner
|
||||
version](https://github.com/actions/virtual-environments) that supports this
|
||||
version of Node or newer.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
run:
|
||||
# ...
|
||||
job_id:
|
||||
# Any runner supporting Node 20 or newer
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Add "id-token" with the intended permissions.
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- id: 'google-cloud-auth'
|
||||
name: 'Authenticate to Google Cloud'
|
||||
uses: 'github.com/sethvargo/oidc-auth-google-cloud'
|
||||
with:
|
||||
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
|
||||
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
|
||||
- uses: 'actions/checkout@v4'
|
||||
|
||||
# Example of using the output:
|
||||
- id: 'access-secret'
|
||||
run: |-
|
||||
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
|
||||
--header "Authorization: Bearer ${{ steps.integration.outputs.access_token }}"
|
||||
- uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
project_id: 'my-project'
|
||||
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Changing the `permissions` block may remove some default permissions. See the
|
||||
> [permissions documentation][github-perms] for more information.
|
||||
|
||||
For more usage options, see the [examples](docs/EXAMPLES.md).
|
||||
|
||||
|
||||
## Inputs
|
||||
|
||||
### Inputs: Workload Identity Federation
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> This option is [not supported by Firebase Admin
|
||||
> SDK](https://github.com/firebase/firebase-admin-node/issues/1377). Use Service
|
||||
> Account Key JSON authentication instead.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> As of the time of this writing, the GitHub OIDC token expires in 5 minutes,
|
||||
> which means any derived credentials also expire in 5 minutes.
|
||||
|
||||
|
||||
The following inputs are for _authenticating_ to Google Cloud via Workload
|
||||
Identity Federation.
|
||||
|
||||
- `workload_identity_provider`: (Required) The full identifier of the Workload
|
||||
Identity Provider, including the project number, pool name, and provider
|
||||
name. This must be the full identifier which includes all parts, for
|
||||
example:
|
||||
name. If provided, this must be the full identifier which includes all
|
||||
parts:
|
||||
|
||||
```text
|
||||
projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider
|
||||
```
|
||||
|
||||
- `service_account`: (Required) Email address or unique identifier of the
|
||||
Google Cloud service account for which to generate credentials. For example:
|
||||
- `service_account`: (Optional) Email address or unique identifier of the
|
||||
Google Cloud service account for which to impersonate and generate
|
||||
credentials. For example:
|
||||
|
||||
```text
|
||||
my-service-account@my-project.iam.gserviceaccount.com
|
||||
```
|
||||
|
||||
Without this input, the GitHub Action will use [Direct Workload Identity
|
||||
Federation](#direct-wif). If this input is provided, the GitHub Action will use
|
||||
[Workload Identity Federation through a Service Account](#indirect-wif).
|
||||
|
||||
- `audience`: (Optional) The value for the audience (`aud`) parameter in the
|
||||
generated GitHub Actions OIDC token. At present, the only valid value is
|
||||
`"sigstore"`, but this variable exists in case custom values are permitted
|
||||
in the future. The default value is `"sigstore"`.
|
||||
generated GitHub Actions OIDC token. This value defaults to the value of
|
||||
`workload_identity_provider`, which is also the default value Google Cloud
|
||||
expects for the audience parameter on the token.
|
||||
|
||||
### Inputs: Service Account Key JSON
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Service Account Key JSON credentials are long-lived credentials and must be
|
||||
> treated like a password.
|
||||
|
||||
The following inputs are for _authenticating_ to Google Cloud via a Service
|
||||
Account Key JSON.
|
||||
|
||||
- `credentials_json`: (Required) The Google Cloud Service Account Key JSON to
|
||||
use for authentication.
|
||||
|
||||
We advise minifying your JSON into a single line string before storing it in
|
||||
a GitHub Secret. When a GitHub Secret is used in a GitHub Actions workflow,
|
||||
_each line_ of the secret is masked in log output. This can lead to
|
||||
aggressive sanitization of benign characters like curly braces (`{}`) and
|
||||
brackets (`[]`).
|
||||
|
||||
To generate access tokens or ID tokens using this service account, you must
|
||||
grant the underlying service account `roles/iam.serviceAccountTokenCreator`
|
||||
permissions on itself.
|
||||
|
||||
### Inputs: Generating OAuth 2.0 access tokens
|
||||
|
||||
The following inputs are for _generating_ OAuth 2.0 access tokens for
|
||||
authenticating to Google Cloud as an output for use in future steps in the
|
||||
workflow. These options only apply to access tokens generated by this action. By
|
||||
default, this action does not generate any tokens.
|
||||
|
||||
- `service_account`: (Required) Email address or unique identifier of the
|
||||
Google Cloud service account for which to generate the access token. For
|
||||
example:
|
||||
|
||||
```text
|
||||
my-service-account@my-project.iam.gserviceaccount.com
|
||||
```
|
||||
|
||||
- `token_format`: (Required) This value must be `"access_token"` to generate
|
||||
OAuth 2.0 access tokens.
|
||||
|
||||
- `access_token_lifetime`: (Optional) Desired lifetime duration of the access
|
||||
token, in seconds. This must be specified as the number of seconds with a
|
||||
trailing "s" (e.g. 30s). The default value is 1 hour (3600s). The maximum
|
||||
value is 1 hour, unless the
|
||||
`constraints/iam.allowServiceAccountCredentialLifetimeExtension`
|
||||
organization policy is enabled, in which case the maximum value is 12 hours.
|
||||
|
||||
- `access_token_scopes`: (Optional) List of OAuth 2.0 access scopes to be
|
||||
included in the generated token. This is only valid when "token_format" is
|
||||
"access_token". The default value is:
|
||||
|
||||
```text
|
||||
https://www.googleapis.com/auth/cloud-platform
|
||||
```
|
||||
|
||||
This can be specified as a comma-separated or newline-separated list.
|
||||
|
||||
- `access_token_subject`: (Optional) Email address of a user to impersonate
|
||||
for [Domain-Wide Delegation][dwd]. Access tokens created for Domain-Wide
|
||||
Delegation cannot have a lifetime beyond 1 hour, even if the
|
||||
`constraints/iam.allowServiceAccountCredentialLifetimeExtension`
|
||||
organization policy is enabled.
|
||||
|
||||
In order to support Domain-Wide Delegation via Workload Identity Federation,
|
||||
you must grant the external identity ("principalSet")
|
||||
`roles/iam.serviceAccountTokenCreator` in addition to
|
||||
`roles/iam.workloadIdentityUser`. The default Workload Identity setup will
|
||||
only grant the latter role. If you want to use this GitHub Action with
|
||||
Domain-Wide Delegation, you must manually add the "Service Account Token
|
||||
Creator" role onto the external identity.
|
||||
|
||||
You will also need to customize the `access_token_scopes` value to
|
||||
correspond to the OAuth scopes required for the API(s) you will access.
|
||||
|
||||
### Inputs: Generating ID tokens
|
||||
|
||||
The following inputs are for _generating_ ID tokens for authenticating to Google
|
||||
Cloud as an output for use in future steps in the workflow. These options only
|
||||
apply to ID tokens generated by this action. By default, this action does not
|
||||
generate any tokens.
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> ID Tokens have a maximum lifetime of 10 minutes. This value cannot be changed.
|
||||
|
||||
- `service_account`: (Required) Email address or unique identifier of the
|
||||
Google Cloud service account for which to generate the ID token. For
|
||||
example:
|
||||
|
||||
```text
|
||||
my-service-account@my-project.iam.gserviceaccount.com
|
||||
```
|
||||
|
||||
- `token_format`: This value must be `"id_token"` to generate ID tokens.
|
||||
|
||||
- `id_token_audience`: (Required) The audience for the generated ID Token.
|
||||
|
||||
- `id_token_include_email`: (Optional) Optional parameter of whether to
|
||||
include the service account email in the generated token. If true, the token
|
||||
will contain "email" and "email_verified" claims. This is only valid when
|
||||
"token_format" is "id_token". The default value is false.
|
||||
|
||||
### Inputs: Miscellaneous
|
||||
|
||||
The following inputs are for controlling the behavior of this GitHub Actions,
|
||||
regardless of the authentication mechanism.
|
||||
|
||||
- `project_id`: (Optional) Custom project ID to use for authentication and
|
||||
exporting into other steps. If unspecified, we will attempt to extract the
|
||||
project ID from the Workload Identity Provider, Service Account email, or
|
||||
the Service Account Key JSON. If this fails, you will need to specify the
|
||||
project ID manually.
|
||||
|
||||
- `create_credentials_file`: (Optional) If true, the action will securely
|
||||
generate a credentials file which can be used for authentication via gcloud
|
||||
and Google Cloud SDKs in other steps in the workflow. The default is true.
|
||||
|
||||
The credentials file is exported into `$GITHUB_WORKSPACE`, which makes it
|
||||
available to all future steps and filesystems (including Docker-based GitHub
|
||||
Actions). The file is automatically removed at the end of the job via a post
|
||||
action. In order to use exported credentials, you **must** add the
|
||||
`actions/checkout` step before calling `auth`. This is due to how GitHub
|
||||
Actions creates `$GITHUB_WORKSPACE`:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
job_id:
|
||||
steps:
|
||||
- uses: 'actions/checkout@v4' # Must come first!
|
||||
- uses: 'google-github-actions/auth@v3'
|
||||
```
|
||||
|
||||
- `export_environment_variables`: (Optional) If true, the action will export
|
||||
common environment variables which are known to be consumed by popular
|
||||
downstream libraries and tools, including:
|
||||
|
||||
- `CLOUDSDK_PROJECT`
|
||||
- `CLOUDSDK_CORE_PROJECT`
|
||||
- `GCP_PROJECT`
|
||||
- `GCLOUD_PROJECT`
|
||||
- `GOOGLE_CLOUD_PROJECT`
|
||||
|
||||
If `create_credentials_file` is true, additional environment variables are
|
||||
exported:
|
||||
|
||||
- `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`
|
||||
- `GOOGLE_APPLICATION_CREDENTIALS`
|
||||
- `GOOGLE_GHA_CREDS_PATH`
|
||||
|
||||
If false, the action will not export any environment variables, meaning
|
||||
future steps are unlikely to be automatically authenticated to Google Cloud.
|
||||
The default value is true.
|
||||
|
||||
- `delegates`: (Optional) List of additional service account emails or unique
|
||||
identities to use for impersonation in the chain. By default there are no
|
||||
delegates.
|
||||
delegates. This can be specified as a comma-separated or newline-separated
|
||||
list.
|
||||
|
||||
- `lifetime`: (Optional) Desired lifetime duration of the access token, in
|
||||
seconds. This must be specified as the number of seconds with a trailing "s"
|
||||
(e.g. 30s). The default value is 1 hour (3600s).
|
||||
- `universe`: (Optional) 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.
|
||||
|
||||
You can also override individual API endpoints by setting the environment
|
||||
variable `GHA_ENDPOINT_OVERRIDE_<endpoint>` where endpoint is the API
|
||||
endpoint to override. This only applies to the `auth` action and does not
|
||||
persist to other steps. For example:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
GHA_ENDPOINT_OVERRIDE_oauth2: 'https://oauth2.myapi.endpoint/v1'
|
||||
```
|
||||
|
||||
- `request_reason`: (Optional) An optional Reason Request [System
|
||||
Parameter](https://cloud.google.com/apis/docs/system-parameters) 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.
|
||||
|
||||
- `cleanup_credentials`: (Optional) If true, the action will remove any
|
||||
created credentials from the filesystem upon completion. This only applies
|
||||
if "create_credentials_file" is true. The default is true.
|
||||
|
||||
## Outputs
|
||||
|
||||
- `access_token`: The authenticated Google Cloud access token for calling
|
||||
other Google Cloud APIs.
|
||||
- `project_id`: Provided or extracted value for the Google Cloud project ID.
|
||||
|
||||
- `expiration`: The RFC3339 UTC "Zulu" format timestamp when the token
|
||||
expires.
|
||||
- `credentials_file_path`: 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`: The Google Cloud federated token (for Workload Identity
|
||||
Federation) or self-signed JWT (for a Service Account Key JSON). This output
|
||||
is always available.
|
||||
|
||||
- `access_token`: The Google Cloud access token for calling other Google Cloud
|
||||
APIs. This is only available when "token_format" is "access_token".
|
||||
|
||||
- `id_token`: The Google Cloud ID token. This is only available when
|
||||
"token_format" is "id_token".
|
||||
|
||||
|
||||
<a id="setup"></a>
|
||||
## Setup
|
||||
|
||||
To exchange a GitHub Actions OIDC token for a Google Cloud access token, you
|
||||
must create and configure a Workload Identity Provider. These instructions use
|
||||
the [gcloud][gcloud] command-line tool.
|
||||
This section describes the three configuration options:
|
||||
|
||||
1. Create or use an existing Google Cloud project. You must have privileges to
|
||||
create Workload Identity Pools, Workload Identity Providers, and to manage
|
||||
Service Accounts and IAM permissions. Save your project ID as an environment
|
||||
variable. The rest of these steps assume this environment variable is set:
|
||||
1. [(Preferred) Direct Workload Identity Federation](#direct-wif)
|
||||
1. [Workload Identity Federation through a Service Account](#indirect-wif)
|
||||
1. [Service Account Key JSON](#sake)
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> It can take up to 5 minutes for Workload Identity Pools, Workload Identity
|
||||
> Providers, and IAM permissions to propagate. Please wait at least five minutes
|
||||
> and follow all [Troubleshooting steps](docs/TROUBLESHOOTING.md) before opening
|
||||
> an issue.
|
||||
|
||||
|
||||
<a name="direct-wif" id="direct-wif"></a>
|
||||
### (Preferred) Direct Workload Identity Federation
|
||||
|
||||
In this setup, the Workload Identity Pool has direct IAM permissions on Google
|
||||
Cloud resources; there are no intermediate service accounts or keys. This is
|
||||
preferred since it directly authenticates GitHub Actions to Google Cloud without
|
||||
a proxy resource. However, not all Google Cloud resources support `principalSet`
|
||||
identities, and the resulting token has a maximum lifetime of 10 minutes. Please
|
||||
see the documentation for your Google Cloud service for more information.
|
||||
|
||||
[](docs/google-github-actions-auth-direct-workload-identity-federation.svg)
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> To generate OAuth 2.0 access tokens or ID tokens, you _must_ provide a service
|
||||
> account email, and the Workload Identity Pool must have
|
||||
> `roles/iam.workloadIdentityUser` permissions on the target Google Cloud
|
||||
> Service Account. Follow the steps for Workload Identity Federation through a
|
||||
> Service Account instead.
|
||||
|
||||
<details>
|
||||
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a direct Workload Identity Federation.</summary>
|
||||
|
||||
These instructions use the [gcloud][gcloud] command-line tool.
|
||||
|
||||
1. Create a Workload Identity Pool:
|
||||
|
||||
```sh
|
||||
export PROJECT_ID="my-project" # update with your value
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam workload-identity-pools create "github" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--display-name="GitHub Actions Pool"
|
||||
```
|
||||
|
||||
1. Get the full ID of the Workload Identity **Pool**:
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam workload-identity-pools describe "github" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--format="value(name)"
|
||||
```
|
||||
|
||||
This value should be of the format:
|
||||
|
||||
```text
|
||||
projects/123456789/locations/global/workloadIdentityPools/github
|
||||
```
|
||||
|
||||
1. Create a Workload Identity **Provider** in that pool:
|
||||
|
||||
**🛑 CAUTION!** Always add an Attribute Condition to restrict entry into the
|
||||
Workload Identity Pool. You can further restrict access in IAM Bindings, but
|
||||
always add a basic condition that restricts admission into the pool. A good
|
||||
default option is to restrict admission based on your GitHub organization as
|
||||
demonstrated below. Please see the [security
|
||||
considerations][security-considerations] for more details.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} and ${GITHUB_ORG} with your values below.
|
||||
|
||||
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--workload-identity-pool="github" \
|
||||
--display-name="My GitHub repo Provider" \
|
||||
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
|
||||
--attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'" \
|
||||
--issuer-uri="https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
> **❗️ IMPORTANT** You must map any claims in the incoming token to
|
||||
> attributes before you can assert on those attributes in a CEL expression
|
||||
> or IAM policy!
|
||||
|
||||
1. Extract the Workload Identity **Provider** resource name:
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam workload-identity-pools providers describe "my-repo" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--workload-identity-pool="github" \
|
||||
--format="value(name)"
|
||||
```
|
||||
|
||||
Use this value as the `workload_identity_provider` value in the GitHub
|
||||
Actions YAML:
|
||||
|
||||
```yaml
|
||||
- uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
project_id: 'my-project'
|
||||
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
|
||||
```
|
||||
|
||||
> **❗️ IMPORTANT** The `project_id` input is optional, but may be required
|
||||
> by downstream authentication systems such as the `gcloud` CLI.
|
||||
> Unfortunately we cannot extract the project ID from the Workload Identity
|
||||
> Provider, since it requires the project _number_.
|
||||
>
|
||||
> It is technically possible to convert a project _number_ into a project
|
||||
> _ID_, but it requires permissions to call Cloud Resource Manager, and we
|
||||
> cannot guarantee that the Workload Identity Pool has those permissions.
|
||||
|
||||
1. As needed, allow authentications from the Workload Identity Pool to Google
|
||||
Cloud resources. These can be any Google Cloud resources that support
|
||||
federated ID tokens, and it can be done after the GitHub Action is
|
||||
configured.
|
||||
|
||||
The following example shows granting access from a GitHub Action in a
|
||||
specific repository a secret in Google Secret Manager.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID}, ${WORKLOAD_IDENTITY_POOL_ID}, and ${REPO}
|
||||
# with your values below.
|
||||
#
|
||||
# ${REPO} is the full repo name including the parent GitHub organization,
|
||||
# such as "my-org/my-repo".
|
||||
#
|
||||
# ${WORKLOAD_IDENTITY_POOL_ID} is the full pool id, such as
|
||||
# "projects/123456789/locations/global/workloadIdentityPools/github".
|
||||
|
||||
gcloud secrets add-iam-policy-binding "my-secret" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
|
||||
```
|
||||
|
||||
Review the [GitHub documentation][github-oidc] for a complete list of
|
||||
options and values. This GitHub repository does not seek to enumerate every
|
||||
possible combination.
|
||||
</details>
|
||||
|
||||
|
||||
<a name="indirect-wif" id="indirect-wif"></a>
|
||||
### Workload Identity Federation through a Service Account
|
||||
|
||||
In this setup, the Workload Identity Pool impersonates a Google Cloud Service
|
||||
Account which has IAM permissions on Google Cloud resources. This exchanges the
|
||||
GitHub Actions OIDC token with a Google Cloud OAuth 2.0 access token by granting
|
||||
GitHub Actions permissions to mint tokens for the given Service Account. Thus
|
||||
GitHub Actions inherits that Service Account's permissions by proxy.
|
||||
|
||||
[](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)
|
||||
|
||||
<details>
|
||||
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Workload Identity Federation through a Service Account.</summary>
|
||||
|
||||
These instructions use the [gcloud][gcloud] command-line tool.
|
||||
|
||||
1. (Optional) Create a Google Cloud Service Account. If you already have a
|
||||
Service Account, take note of the email address and skip this step.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam service-accounts create "my-service-account" \
|
||||
--project "${PROJECT_ID}"
|
||||
```
|
||||
|
||||
1. (Optional) Grant the Google Cloud Service Account permissions to access
|
||||
Google Cloud resources. This step varies by use case. For demonstration
|
||||
purposes, you could grant access to a Google Secret Manager secret or Google
|
||||
Cloud Storage object.
|
||||
|
||||
1. Create a Workload Identity Pool:
|
||||
|
||||
```sh
|
||||
gcloud iam workload-identity-pools create "my-pool" \
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam workload-identity-pools create "github" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--display-name="Demo pool"
|
||||
--display-name="GitHub Actions Pool"
|
||||
```
|
||||
|
||||
1. Create a Workload Identity Provider in that pool:
|
||||
1. Get the full ID of the Workload Identity **Pool**:
|
||||
|
||||
```sh
|
||||
gcloud iam workload-identity-pools providers create-oidc "my-provider" \
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam workload-identity-pools describe "github" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--workload-identity-pool="my-pool" \
|
||||
--display-name="Demo provider" \
|
||||
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.aud=assertion.aud" \
|
||||
--issuer-uri="https://vstoken.actions.githubusercontent.com" \
|
||||
--allowed-audiences="sigstore"
|
||||
--format="value(name)"
|
||||
```
|
||||
|
||||
- The audience of "sigstore" is currently the only value GitHub allows.
|
||||
- The attribute mappings map claims in the GitHub Actions JWT to
|
||||
assertions you can make about the request (like the repository or GitHub
|
||||
username of the principal invoking the GitHub Action). These can be used
|
||||
to further restrict the authentication using `--attribute-condition`
|
||||
flags.
|
||||
|
||||
1. Get the full ID for the Workload Identity Provider:
|
||||
|
||||
```sh
|
||||
gcloud iam workload-identity-pools providers describe "my-provider" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--workload-identity-pool="my-pool"
|
||||
```
|
||||
|
||||
Take note of the `name` attribute. It will be of the format:
|
||||
This value should be of the format:
|
||||
|
||||
```text
|
||||
projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider
|
||||
projects/123456789/locations/global/workloadIdentityPools/github
|
||||
```
|
||||
|
||||
Save this value as an environment variable:
|
||||
1. Create a Workload Identity **Provider** in that pool:
|
||||
|
||||
**🛑 CAUTION!** Always add an Attribute Condition to restrict entry into the
|
||||
Workload Identity Pool. You can further restrict access in IAM Bindings, but
|
||||
always add a basic condition that restricts admission into the pool. A good
|
||||
default option is to restrict admission based on your GitHub organization as
|
||||
demonstrated below. Please see the [security
|
||||
considerations][security-considerations] for more details.
|
||||
|
||||
```sh
|
||||
export WORKLOAD_IDENTITY_PROVIDER_ID="..." # value from above
|
||||
# TODO: replace ${PROJECT_ID} and ${GITHUB_ORG} with your values below.
|
||||
|
||||
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--workload-identity-pool="github" \
|
||||
--display-name="My GitHub repo Provider" \
|
||||
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
|
||||
--attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'" \
|
||||
--issuer-uri="https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
1. Allow authentications from the Workload Identity Provider to impersonate the
|
||||
Service Account created above:
|
||||
> **❗️ IMPORTANT** You must map any claims in the incoming token to
|
||||
> attributes before you can assert on those attributes in a CEL expression
|
||||
> or IAM policy!
|
||||
|
||||
**Warning**: This grants access to any resource in the pool (all GitHub
|
||||
repos). It's **strongly recommended** that you map to a specific attribute
|
||||
such as the actor or repository name instead. See [mapping external
|
||||
identities][map-external] for more information.
|
||||
1. Allow authentications from the Workload Identity Pool to your Google Cloud
|
||||
Service Account.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID}, ${WORKLOAD_IDENTITY_POOL_ID}, and ${REPO}
|
||||
# with your values below.
|
||||
#
|
||||
# ${REPO} is the full repo name including the parent GitHub organization,
|
||||
# such as "my-org/my-repo".
|
||||
#
|
||||
# ${WORKLOAD_IDENTITY_POOL_ID} is the full pool id, such as
|
||||
# "projects/123456789/locations/global/workloadIdentityPools/github".
|
||||
|
||||
gcloud iam service-accounts add-iam-policy-binding "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--role="roles/iam.workloadIdentityUser" \
|
||||
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_PROVIDER_ID}/*"
|
||||
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
|
||||
```
|
||||
|
||||
To map to a specific repository:
|
||||
Review the [GitHub documentation][github-oidc] for a complete list of
|
||||
options and values. This GitHub repository does not seek to enumerate every
|
||||
possible combination.
|
||||
|
||||
1. Extract the Workload Identity **Provider** resource name:
|
||||
|
||||
```sh
|
||||
gcloud iam service-accounts add-iam-policy-binding "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com" \
|
||||
--role="roles/iam.workloadIdentityUser" \
|
||||
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_PROVIDER_ID}/attribute.repo/my-repo"
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam workload-identity-pools providers describe "my-repo" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="global" \
|
||||
--workload-identity-pool="github" \
|
||||
--format="value(name)"
|
||||
```
|
||||
|
||||
1. Use this GitHub Action with the Workload Identity Provider ID and Service
|
||||
Account email. The GitHub Action will mint a GitHub OIDC token and exchange
|
||||
the GitHub token for a Google Cloud access token (assuming the authorization
|
||||
is correct). This all happens without exporting a Google Cloud service
|
||||
account key JSON!
|
||||
Use this value as the `workload_identity_provider` value in the GitHub
|
||||
Actions YAML:
|
||||
|
||||
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation
|
||||
```yaml
|
||||
- uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
service_account: '...' # my-service-account@my-project.iam.gserviceaccount.com
|
||||
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
|
||||
```
|
||||
|
||||
1. As needed, grant the Google Cloud Service Account permissions to access
|
||||
Google Cloud resources. This step varies by use case. The following example
|
||||
shows granting access to a secret in Google Secret Manager.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud secrets add-iam-policy-binding "my-secret" \
|
||||
--project="${PROJECT_ID}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--member="serviceAccount:my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<a name="sake" id="sake"></a>
|
||||
### Service Account Key JSON
|
||||
|
||||
In this setup, a Service Account has direct IAM permissions on Google Cloud
|
||||
resources. You download a Service Account Key JSON file and upload it to GitHub
|
||||
as a secret.
|
||||
|
||||
[](docs/google-github-actions-auth-service-account-key-export.svg)
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Google Cloud Service Account Key JSON files must be secured
|
||||
> and treated like a password. Anyone with access to the JSON key can
|
||||
> authenticate to Google Cloud as the underlying Service Account. By default,
|
||||
> these credentials never expire, which is why the former authentication options
|
||||
> are much preferred.
|
||||
|
||||
<details>
|
||||
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Service Account Key JSON.</summary>
|
||||
|
||||
These instructions use the [gcloud][gcloud] command-line tool.
|
||||
|
||||
1. (Optional) Create a Google Cloud Service Account. If you already have a
|
||||
Service Account, take note of the email address and skip this step.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam service-accounts create "my-service-account" \
|
||||
--project "${PROJECT_ID}"
|
||||
```
|
||||
|
||||
1. Create a Service Account Key JSON for the Service Account.
|
||||
|
||||
```sh
|
||||
# TODO: replace ${PROJECT_ID} with your value below.
|
||||
|
||||
gcloud iam service-accounts keys create "key.json" \
|
||||
--iam-account "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
|
||||
```
|
||||
|
||||
1. Upload the contents of this file as a [GitHub Actions
|
||||
Secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
|
||||
|
||||
Use the name of the GitHub Actions secret as the `credentials_json` value in
|
||||
the GitHub Actions YAML:
|
||||
|
||||
```yaml
|
||||
- uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' # Replace with the name of your GitHub Actions secret
|
||||
```
|
||||
</details>
|
||||
|
||||
[dwd]: https://developers.google.com/admin-sdk/directory/v1/guides/delegation
|
||||
[gcloud]: https://cloud.google.com/sdk
|
||||
[github-oidc]: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token
|
||||
[github-perms]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
|
||||
[map-external]: https://cloud.google.com/iam/docs/access-resources-oidc#impersonate
|
||||
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation
|
||||
[security-considerations]: docs/SECURITY_CONSIDERATIONS.md
|
||||
|
|
|
|||
155
action.yml
155
action.yml
|
|
@ -12,31 +12,81 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: 'OIDC Authenticate to Google Cloud'
|
||||
author: 'sethvargo'
|
||||
name: 'Authenticate to Google Cloud'
|
||||
author: 'Google LLC'
|
||||
description: |-
|
||||
Authenticate to Google Cloud from GitHub Actions using an OIDC token and
|
||||
Workload Identity Federation.
|
||||
Authenticate to Google Cloud from GitHub Actions via Workload Identity
|
||||
Federation or service account keys.
|
||||
|
||||
inputs:
|
||||
project_id:
|
||||
description: |-
|
||||
ID of the default project to use for future API calls and invocations. If
|
||||
unspecified, this action will attempt to extract the value from other
|
||||
inputs such as "service_account" or "credentials_json".
|
||||
required: false
|
||||
workload_identity_provider:
|
||||
description: |-
|
||||
The full identifier of the Workload Identity Provider, including the
|
||||
project number, pool name, and provider name. This must be the full
|
||||
identifier which includes all parts, for example:
|
||||
project number, pool name, and provider name. If provided, this must be
|
||||
the full identifier which includes all parts, for example:
|
||||
"projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider".
|
||||
required: true
|
||||
This is mutually exclusive with "credentials_json".
|
||||
required: false
|
||||
service_account:
|
||||
description: |-
|
||||
Email address or unique identifier of the Google Cloud service account for
|
||||
which to generate credentials.
|
||||
required: true
|
||||
which to generate credentials. This is required if
|
||||
"workload_identity_provider" is specified.
|
||||
required: false
|
||||
audience:
|
||||
description: |-
|
||||
The value for the audience (aud) parameter in GitHub's generated OIDC
|
||||
token. At present, the only valid value is "sigstore", but this variable
|
||||
exists in case custom values are permitted in the future.
|
||||
default: 'sigstore'
|
||||
token. This value defaults to the value of "workload_identity_provider",
|
||||
which is also the default value Google Cloud expects for the audience
|
||||
parameter on the token.
|
||||
default: ''
|
||||
required: false
|
||||
credentials_json:
|
||||
description: |-
|
||||
The Google Cloud JSON service account key to use for authentication. This
|
||||
is mutually exclusive with "workload_identity_provider".
|
||||
required: false
|
||||
create_credentials_file:
|
||||
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'
|
||||
required: false
|
||||
export_environment_variables:
|
||||
description: |-
|
||||
If true, the action will export common environment variables which are
|
||||
known to be consumed by popular downstream libraries and tools, including:
|
||||
|
||||
- CLOUDSDK_PROJECT
|
||||
- CLOUDSDK_CORE_PROJECT
|
||||
- GCP_PROJECT
|
||||
- GCLOUD_PROJECT
|
||||
- GOOGLE_CLOUD_PROJECT
|
||||
|
||||
If "create_credentials_file" is true, additional environment variables are
|
||||
exported:
|
||||
|
||||
- CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE
|
||||
- GOOGLE_APPLICATION_CREDENTIALS
|
||||
- GOOGLE_GHA_CREDS_PATH
|
||||
|
||||
If false, the action will not export any environment variables, meaning
|
||||
future steps are unlikely to be automatically authenticated to Google
|
||||
Cloud.
|
||||
default: 'true'
|
||||
required: false
|
||||
token_format:
|
||||
description: |-
|
||||
Output format for the generated authentication token. For OAuth 2.0 access
|
||||
tokens, specify "access_token". For OIDC tokens, specify "id_token". To
|
||||
skip token generation, leave this value empty.
|
||||
default: ''
|
||||
required: false
|
||||
delegates:
|
||||
description: |-
|
||||
|
|
@ -44,25 +94,92 @@ inputs:
|
|||
impersonation in the chain.
|
||||
default: ''
|
||||
required: false
|
||||
lifetime:
|
||||
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'
|
||||
required: false
|
||||
|
||||
# access token params
|
||||
access_token_lifetime:
|
||||
description: |-
|
||||
Desired lifetime duration of the access token, in seconds. This must be
|
||||
specified as the number of seconds with a trailing "s" (e.g. 30s).
|
||||
specified as the number of seconds with a trailing "s" (e.g. 30s). This is
|
||||
only valid when "token_format" is "access_token".
|
||||
default: '3600s'
|
||||
required: false
|
||||
access_token_scopes:
|
||||
description: |-
|
||||
List of OAuth 2.0 access scopes to be included in the generated token.
|
||||
This is only valid when "token_format" is "access_token".
|
||||
default: 'https://www.googleapis.com/auth/cloud-platform'
|
||||
required: false
|
||||
access_token_subject:
|
||||
description: |-
|
||||
Email address of a user to impersonate for Domain-Wide Delegation Access
|
||||
tokens created for Domain-Wide Delegation cannot have a lifetime beyond 1
|
||||
hour. This is only valid when "token_format" is "access_token".
|
||||
default: ''
|
||||
required: false
|
||||
|
||||
# id token params
|
||||
id_token_audience:
|
||||
description: |-
|
||||
The audience (aud) for the generated Google Cloud ID Token. This is only
|
||||
valid when "token_format" is "id_token".
|
||||
default: ''
|
||||
required: false
|
||||
id_token_include_email:
|
||||
description: |-
|
||||
Optional parameter of whether to include the service account email in the
|
||||
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'
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
project_id:
|
||||
description: |-
|
||||
Provided or extracted value for the Google Cloud project ID.
|
||||
credentials_file_path:
|
||||
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.
|
||||
expiration:
|
||||
The Google Cloud access token for calling other Google Cloud APIs. This is
|
||||
only available when "token_format" is "access_token".
|
||||
id_token:
|
||||
description: |-
|
||||
The expiration timestamp for the access token.
|
||||
The Google Cloud ID token. This is only available when "token_format" is
|
||||
"id_token".
|
||||
|
||||
branding:
|
||||
icon: 'lock'
|
||||
color: 'blue'
|
||||
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
using: 'node24'
|
||||
main: 'dist/main/index.js'
|
||||
post: 'dist/post/index.js'
|
||||
|
|
|
|||
868
dist/index.js
vendored
868
dist/index.js
vendored
|
|
@ -1,868 +0,0 @@
|
|||
module.exports =
|
||||
/******/ (function(modules, runtime) { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ var threw = true;
|
||||
/******/ try {
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/ threw = false;
|
||||
/******/ } finally {
|
||||
/******/ if(threw) delete installedModules[moduleId];
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ __webpack_require__.ab = __dirname + "/";
|
||||
/******/
|
||||
/******/ // the startup function
|
||||
/******/ function startup() {
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(131);
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // run startup
|
||||
/******/ return startup();
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ({
|
||||
|
||||
/***/ 82:
|
||||
/***/ (function(__unusedmodule, exports) {
|
||||
|
||||
"use strict";
|
||||
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toCommandProperties = exports.toCommandValue = void 0;
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
function toCommandValue(input) {
|
||||
if (input === null || input === undefined) {
|
||||
return '';
|
||||
}
|
||||
else if (typeof input === 'string' || input instanceof String) {
|
||||
return input;
|
||||
}
|
||||
return JSON.stringify(input);
|
||||
}
|
||||
exports.toCommandValue = toCommandValue;
|
||||
/**
|
||||
*
|
||||
* @param annotationProperties
|
||||
* @returns The command properties to send with the actual annotation command
|
||||
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||
*/
|
||||
function toCommandProperties(annotationProperties) {
|
||||
if (!Object.keys(annotationProperties).length) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
title: annotationProperties.title,
|
||||
line: annotationProperties.startLine,
|
||||
endLine: annotationProperties.endLine,
|
||||
col: annotationProperties.startColumn,
|
||||
endColumn: annotationProperties.endColumn
|
||||
};
|
||||
}
|
||||
exports.toCommandProperties = toCommandProperties;
|
||||
//# sourceMappingURL=utils.js.map
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 87:
|
||||
/***/ (function(module) {
|
||||
|
||||
module.exports = require("os");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 102:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
// For internal use, subject to change.
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.issueCommand = void 0;
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const fs = __importStar(__webpack_require__(747));
|
||||
const os = __importStar(__webpack_require__(87));
|
||||
const utils_1 = __webpack_require__(82);
|
||||
function issueCommand(command, message) {
|
||||
const filePath = process.env[`GITHUB_${command}`];
|
||||
if (!filePath) {
|
||||
throw new Error(`Unable to find environment variable for file command ${command}`);
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing file at path: ${filePath}`);
|
||||
}
|
||||
fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
exports.issueCommand = issueCommand;
|
||||
//# sourceMappingURL=file-command.js.map
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 131:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(__webpack_require__(470));
|
||||
const client_1 = __webpack_require__(976);
|
||||
/**
|
||||
* Converts a multi-line or comma-separated collection of strings into an array
|
||||
* of trimmed strings.
|
||||
*/
|
||||
function explodeStrings(input) {
|
||||
if (input == null || input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const list = new Array();
|
||||
for (const line of input.split(`\n`)) {
|
||||
for (const piece of line.split(',')) {
|
||||
const entry = piece.trim();
|
||||
if (entry !== '') {
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
/**
|
||||
* Executes the main action, documented inline.
|
||||
*/
|
||||
function run() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
try {
|
||||
// Load configuration.
|
||||
const workloadIdentityProvider = core.getInput('workload_identity_provider', {
|
||||
required: true,
|
||||
});
|
||||
const serviceAccount = core.getInput('service_account', { required: true });
|
||||
const audience = core.getInput('audience');
|
||||
const delegates = explodeStrings(core.getInput('delegates'));
|
||||
const lifetime = core.getInput('lifetime');
|
||||
// Extract the GitHub Actions OIDC token.
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
if (!requestToken) {
|
||||
throw `missing ACTIONS_ID_TOKEN_REQUEST_TOKEN`;
|
||||
}
|
||||
const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!requestURL) {
|
||||
throw `missing ACTIONS_ID_TOKEN_REQUEST_URL`;
|
||||
}
|
||||
const githubOIDCToken = yield client_1.Client.githubToken({
|
||||
url: requestURL,
|
||||
token: requestToken,
|
||||
audience: audience,
|
||||
});
|
||||
core.setSecret(githubOIDCToken);
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = yield client_1.Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
// Exchange the Google Federated Token for an access token.
|
||||
const { accessToken, expiration } = yield client_1.Client.googleAccessToken({
|
||||
token: googleFederatedToken,
|
||||
serviceAccount: serviceAccount,
|
||||
delegates: delegates,
|
||||
lifetime: lifetime,
|
||||
});
|
||||
core.setSecret(accessToken);
|
||||
core.setOutput('access_token', accessToken);
|
||||
core.setOutput('expiration', expiration);
|
||||
}
|
||||
catch (err) {
|
||||
core.setFailed(`Action failed with error: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
run();
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 211:
|
||||
/***/ (function(module) {
|
||||
|
||||
module.exports = require("https");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 431:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.issue = exports.issueCommand = void 0;
|
||||
const os = __importStar(__webpack_require__(87));
|
||||
const utils_1 = __webpack_require__(82);
|
||||
/**
|
||||
* Commands
|
||||
*
|
||||
* Command Format:
|
||||
* ::name key=value,key=value::message
|
||||
*
|
||||
* Examples:
|
||||
* ::warning::This is the message
|
||||
* ::set-env name=MY_VAR::some value
|
||||
*/
|
||||
function issueCommand(command, properties, message) {
|
||||
const cmd = new Command(command, properties, message);
|
||||
process.stdout.write(cmd.toString() + os.EOL);
|
||||
}
|
||||
exports.issueCommand = issueCommand;
|
||||
function issue(name, message = '') {
|
||||
issueCommand(name, {}, message);
|
||||
}
|
||||
exports.issue = issue;
|
||||
const CMD_STRING = '::';
|
||||
class Command {
|
||||
constructor(command, properties, message) {
|
||||
if (!command) {
|
||||
command = 'missing.command';
|
||||
}
|
||||
this.command = command;
|
||||
this.properties = properties;
|
||||
this.message = message;
|
||||
}
|
||||
toString() {
|
||||
let cmdStr = CMD_STRING + this.command;
|
||||
if (this.properties && Object.keys(this.properties).length > 0) {
|
||||
cmdStr += ' ';
|
||||
let first = true;
|
||||
for (const key in this.properties) {
|
||||
if (this.properties.hasOwnProperty(key)) {
|
||||
const val = this.properties[key];
|
||||
if (val) {
|
||||
if (first) {
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
cmdStr += ',';
|
||||
}
|
||||
cmdStr += `${key}=${escapeProperty(val)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cmdStr += `${CMD_STRING}${escapeData(this.message)}`;
|
||||
return cmdStr;
|
||||
}
|
||||
}
|
||||
function escapeData(s) {
|
||||
return utils_1.toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A');
|
||||
}
|
||||
function escapeProperty(s) {
|
||||
return utils_1.toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A')
|
||||
.replace(/:/g, '%3A')
|
||||
.replace(/,/g, '%2C');
|
||||
}
|
||||
//# sourceMappingURL=command.js.map
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 470:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0;
|
||||
const command_1 = __webpack_require__(431);
|
||||
const file_command_1 = __webpack_require__(102);
|
||||
const utils_1 = __webpack_require__(82);
|
||||
const os = __importStar(__webpack_require__(87));
|
||||
const path = __importStar(__webpack_require__(622));
|
||||
/**
|
||||
* The code to exit an action
|
||||
*/
|
||||
var ExitCode;
|
||||
(function (ExitCode) {
|
||||
/**
|
||||
* A code indicating that the action was successful
|
||||
*/
|
||||
ExitCode[ExitCode["Success"] = 0] = "Success";
|
||||
/**
|
||||
* A code indicating that the action was a failure
|
||||
*/
|
||||
ExitCode[ExitCode["Failure"] = 1] = "Failure";
|
||||
})(ExitCode = exports.ExitCode || (exports.ExitCode = {}));
|
||||
//-----------------------------------------------------------------------
|
||||
// Variables
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Sets env variable for this action and future actions in the job
|
||||
* @param name the name of the variable to set
|
||||
* @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function exportVariable(name, val) {
|
||||
const convertedVal = utils_1.toCommandValue(val);
|
||||
process.env[name] = convertedVal;
|
||||
const filePath = process.env['GITHUB_ENV'] || '';
|
||||
if (filePath) {
|
||||
const delimiter = '_GitHubActionsFileCommandDelimeter_';
|
||||
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`;
|
||||
file_command_1.issueCommand('ENV', commandValue);
|
||||
}
|
||||
else {
|
||||
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||
}
|
||||
}
|
||||
exports.exportVariable = exportVariable;
|
||||
/**
|
||||
* Registers a secret which will get masked from logs
|
||||
* @param secret value of the secret
|
||||
*/
|
||||
function setSecret(secret) {
|
||||
command_1.issueCommand('add-mask', {}, secret);
|
||||
}
|
||||
exports.setSecret = setSecret;
|
||||
/**
|
||||
* Prepends inputPath to the PATH (for this action and future actions)
|
||||
* @param inputPath
|
||||
*/
|
||||
function addPath(inputPath) {
|
||||
const filePath = process.env['GITHUB_PATH'] || '';
|
||||
if (filePath) {
|
||||
file_command_1.issueCommand('PATH', inputPath);
|
||||
}
|
||||
else {
|
||||
command_1.issueCommand('add-path', {}, inputPath);
|
||||
}
|
||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
||||
}
|
||||
exports.addPath = addPath;
|
||||
/**
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string
|
||||
*/
|
||||
function getInput(name, options) {
|
||||
const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
|
||||
if (options && options.required && !val) {
|
||||
throw new Error(`Input required and not supplied: ${name}`);
|
||||
}
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return val;
|
||||
}
|
||||
return val.trim();
|
||||
}
|
||||
exports.getInput = getInput;
|
||||
/**
|
||||
* Gets the values of an multiline input. Each value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string[]
|
||||
*
|
||||
*/
|
||||
function getMultilineInput(name, options) {
|
||||
const inputs = getInput(name, options)
|
||||
.split('\n')
|
||||
.filter(x => x !== '');
|
||||
return inputs;
|
||||
}
|
||||
exports.getMultilineInput = getMultilineInput;
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
* The return value is also in boolean type.
|
||||
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns boolean
|
||||
*/
|
||||
function getBooleanInput(name, options) {
|
||||
const trueValue = ['true', 'True', 'TRUE'];
|
||||
const falseValue = ['false', 'False', 'FALSE'];
|
||||
const val = getInput(name, options);
|
||||
if (trueValue.includes(val))
|
||||
return true;
|
||||
if (falseValue.includes(val))
|
||||
return false;
|
||||
throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` +
|
||||
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
||||
}
|
||||
exports.getBooleanInput = getBooleanInput;
|
||||
/**
|
||||
* Sets the value of an output.
|
||||
*
|
||||
* @param name name of the output to set
|
||||
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setOutput(name, value) {
|
||||
process.stdout.write(os.EOL);
|
||||
command_1.issueCommand('set-output', { name }, value);
|
||||
}
|
||||
exports.setOutput = setOutput;
|
||||
/**
|
||||
* Enables or disables the echoing of commands into stdout for the rest of the step.
|
||||
* Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
|
||||
*
|
||||
*/
|
||||
function setCommandEcho(enabled) {
|
||||
command_1.issue('echo', enabled ? 'on' : 'off');
|
||||
}
|
||||
exports.setCommandEcho = setCommandEcho;
|
||||
//-----------------------------------------------------------------------
|
||||
// Results
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Sets the action status to failed.
|
||||
* When the action exits it will be with an exit code of 1
|
||||
* @param message add error issue message
|
||||
*/
|
||||
function setFailed(message) {
|
||||
process.exitCode = ExitCode.Failure;
|
||||
error(message);
|
||||
}
|
||||
exports.setFailed = setFailed;
|
||||
//-----------------------------------------------------------------------
|
||||
// Logging Commands
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Gets whether Actions Step Debug is on or not
|
||||
*/
|
||||
function isDebug() {
|
||||
return process.env['RUNNER_DEBUG'] === '1';
|
||||
}
|
||||
exports.isDebug = isDebug;
|
||||
/**
|
||||
* Writes debug message to user log
|
||||
* @param message debug message
|
||||
*/
|
||||
function debug(message) {
|
||||
command_1.issueCommand('debug', {}, message);
|
||||
}
|
||||
exports.debug = debug;
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @param message error issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function error(message, properties = {}) {
|
||||
command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.error = error;
|
||||
/**
|
||||
* Adds a warning issue
|
||||
* @param message warning issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function warning(message, properties = {}) {
|
||||
command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.warning = warning;
|
||||
/**
|
||||
* Adds a notice issue
|
||||
* @param message notice issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function notice(message, properties = {}) {
|
||||
command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.notice = notice;
|
||||
/**
|
||||
* Writes info to log with console.log.
|
||||
* @param message info message
|
||||
*/
|
||||
function info(message) {
|
||||
process.stdout.write(message + os.EOL);
|
||||
}
|
||||
exports.info = info;
|
||||
/**
|
||||
* Begin an output group.
|
||||
*
|
||||
* Output until the next `groupEnd` will be foldable in this group
|
||||
*
|
||||
* @param name The name of the output group
|
||||
*/
|
||||
function startGroup(name) {
|
||||
command_1.issue('group', name);
|
||||
}
|
||||
exports.startGroup = startGroup;
|
||||
/**
|
||||
* End an output group.
|
||||
*/
|
||||
function endGroup() {
|
||||
command_1.issue('endgroup');
|
||||
}
|
||||
exports.endGroup = endGroup;
|
||||
/**
|
||||
* Wrap an asynchronous function call in a group.
|
||||
*
|
||||
* Returns the same type as the function itself.
|
||||
*
|
||||
* @param name The name of the group
|
||||
* @param fn The function to wrap in the group
|
||||
*/
|
||||
function group(name, fn) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
startGroup(name);
|
||||
let result;
|
||||
try {
|
||||
result = yield fn();
|
||||
}
|
||||
finally {
|
||||
endGroup();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
exports.group = group;
|
||||
//-----------------------------------------------------------------------
|
||||
// Wrapper action state
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
||||
*
|
||||
* @param name name of the state to store
|
||||
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function saveState(name, value) {
|
||||
command_1.issueCommand('save-state', { name }, value);
|
||||
}
|
||||
exports.saveState = saveState;
|
||||
/**
|
||||
* Gets the value of an state set by this action's main execution.
|
||||
*
|
||||
* @param name name of the state to get
|
||||
* @returns string
|
||||
*/
|
||||
function getState(name) {
|
||||
return process.env[`STATE_${name}`] || '';
|
||||
}
|
||||
exports.getState = getState;
|
||||
//# sourceMappingURL=core.js.map
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 622:
|
||||
/***/ (function(module) {
|
||||
|
||||
module.exports = require("path");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 747:
|
||||
/***/ (function(module) {
|
||||
|
||||
module.exports = require("fs");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 835:
|
||||
/***/ (function(module) {
|
||||
|
||||
module.exports = require("url");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 976:
|
||||
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Client = void 0;
|
||||
const https_1 = __importDefault(__webpack_require__(211));
|
||||
const url_1 = __webpack_require__(835);
|
||||
class Client {
|
||||
/**
|
||||
* request is a high-level helper that returns a promise from the executed
|
||||
* request.
|
||||
*/
|
||||
static request(opts, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https_1.default.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let body = '';
|
||||
res.on('data', (data) => {
|
||||
body += data;
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(body);
|
||||
}
|
||||
else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
if (data != null) {
|
||||
req.write(data);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* githubToken invokes the given URL, appending the audience parameter, using
|
||||
* the provided token as authentication. This can only be run from inside a
|
||||
* GitHub Action.
|
||||
*/
|
||||
static githubToken({ url, audience, token }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const requestURL = new url_1.URL(url);
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', audience);
|
||||
requestURL.search = params.toString();
|
||||
// Make the request.
|
||||
const opts = {
|
||||
hostname: requestURL.hostname,
|
||||
port: requestURL.port,
|
||||
path: requestURL.pathname + requestURL.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield Client.request(opts);
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['value'];
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate GitHub OIDC token via ${url} (aud: ${audience}): ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleFederatedToken generates a Google Cloud federated token using the
|
||||
* provided OIDC token and Workload Identity Provider.
|
||||
*/
|
||||
static googleFederatedToken({ providerID, token, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const stsURL = new url_1.URL('https://sts.googleapis.com/v1/token');
|
||||
const data = {
|
||||
audience: '//iam.googleapis.com/' + 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: token,
|
||||
};
|
||||
const opts = {
|
||||
hostname: stsURL.hostname,
|
||||
port: stsURL.port,
|
||||
path: stsURL.pathname + stsURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['access_token'];
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* googleAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static googleAccessToken({ token, serviceAccount, delegates, lifetime, }) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new url_1.URL(`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`);
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
lifetime: lifetime,
|
||||
};
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = yield Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.Client = Client;
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
3
dist/main/index.js
vendored
Normal file
3
dist/main/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/post/index.js
vendored
Normal file
3
dist/post/index.js
vendored
Normal file
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
|
||||
315
docs/TROUBLESHOOTING.md
Normal file
315
docs/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
# Troubleshooting
|
||||
|
||||
## Permission denied
|
||||
|
||||
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.
|
||||
|
||||
> **⚠️ 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.
|
||||
|
||||
1. Ensure you have waited at least 5 minutes between making changes to the
|
||||
Workload Identity Pool, Workload Identity Provider, or IAM policies. Changes
|
||||
to these resources are eventually consistent. Usually they happen
|
||||
immediately, but sometimes they can take up to 5 minutes to propagate.
|
||||
|
||||
1. Ensure `actions/checkout@v4` is **before** the `auth` action in your
|
||||
workflow.
|
||||
|
||||
```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:
|
||||
|
||||
```diff
|
||||
- projects/NUMBER/locations/global/workloadIdentityPools/POOL
|
||||
+ 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 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
|
||||
token", it means admission into the Workload Identity Pool failed. Check
|
||||
your [**Attribute Conditions**][attribute-conditions].
|
||||
|
||||
- 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.
|
||||
> You can disable this audit logging after you have debugged the issue.
|
||||
|
||||
Try to authenticate again, and then explore the logs for your Workload
|
||||
Identity Provider and Workload Identity Pool. Sometimes these error messages
|
||||
are helpful in identifying the root cause.
|
||||
|
||||
1. 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
|
||||
|
||||
If you get an error like:
|
||||
|
||||
```text
|
||||
The size of mapped attribute exceeds the 127 bytes limit.
|
||||
```
|
||||
|
||||
it means that the GitHub OIDC token had a claim that exceeded the maximum
|
||||
allowed value of 127 bytes. In general, 1 byte = 1 character. This most common
|
||||
reason this occurs is due to long repo names or long branch names.
|
||||
|
||||
**This is a limit imposed by Google Cloud IAM.** We have no control over
|
||||
this value. It is documented [here][wif-byte-limit]. Please [file feedback
|
||||
with the Google Cloud IAM team][iam-feedback]. The only mitigation is to use
|
||||
shorter repo names or shorter branch names.
|
||||
|
||||
|
||||
## Token lifetime cannot exceed 1 hour
|
||||
|
||||
If you get an error like:
|
||||
|
||||
```text
|
||||
The access token lifetime cannot exceed 3600 seconds.
|
||||
```
|
||||
|
||||
it means that there is likely clock skew between where you are running the
|
||||
`auth` GitHub Action and Google's servers. You can either install and configure
|
||||
ntp pointed at time.google.com, or adjust the `access_token_lifetime` value to
|
||||
something less than `3600s` to allow for clock skew (`3300s` would allow for 5
|
||||
minutes of clock skew).
|
||||
|
||||
|
||||
## Dirty git or bundled credentials
|
||||
|
||||
By default, the `auth` action exports credentials to the current workspace so
|
||||
that the credentials are automatically available to future steps and
|
||||
Docker-based actions. The credentials file is automatically removed when the job
|
||||
finishes.
|
||||
|
||||
This means, after the `auth` action runs, the workspace is dirty and contains a
|
||||
credentials file. This means creating a pull request, compiling a binary, or
|
||||
building a Docker container, will include said credential file. There are a few
|
||||
ways to fix this issue:
|
||||
|
||||
- Add and commit the following lines to your `.gitignore`:
|
||||
|
||||
```text
|
||||
# Ignore generated credentials from google-github-actions/auth
|
||||
gha-creds-*.json
|
||||
```
|
||||
|
||||
**This requires the `auth` action be v0.6.0 or later.**
|
||||
|
||||
- Re-order your steps. In most cases, you can re-order your steps such
|
||||
that `auth` comes _after_ the "compilation" step:
|
||||
|
||||
```text
|
||||
1. Checkout
|
||||
2. Compile (e.g. "docker build", "go build", "git add")
|
||||
3. Auth
|
||||
4. Push
|
||||
```
|
||||
|
||||
This ensures that no authentication data is present during artifact
|
||||
creation.
|
||||
|
||||
- In situations where `auth` must occur before compilation, you can use
|
||||
the output to exclude the credential:
|
||||
|
||||
```text
|
||||
1. Checkout
|
||||
2. Auth
|
||||
3. Inject "${{ steps.auth.outputs.credentials_file_path }}" into ignore file (e.g. .gitignore, .dockerignore)
|
||||
4. Compile (e.g. "docker build", "go build", "git add")
|
||||
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
|
||||
|
||||
When you use a [GitHub Actions secret][github-secrets] inside a workflow, _each_
|
||||
line of the secret is masked in log output. This is controlled by GitHub, not
|
||||
the `auth` action. We cannot change this behavior.
|
||||
|
||||
This can be problematic if your secret is a multi-line JSON string, since it
|
||||
means curly braces (`{}`) and brackets (`[]`) will likely be replaced as `***`
|
||||
in the GitHub Actions log output. To avoid this, remove all unnecessary
|
||||
whitespace from the JSON and save the secret as a single-line JSON string. You
|
||||
can convert a multi-line JSON document to a single-line manually or by using a
|
||||
tool like `jq`:
|
||||
|
||||
```sh
|
||||
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
|
||||
[debug-logs]: https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging
|
||||
[iam-feedback]: https://cloud.google.com/iam/docs/getting-support
|
||||
[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,
|
||||
);
|
||||
4505
package-lock.json
generated
4505
package-lock.json
generated
File diff suppressed because it is too large
Load diff
53
package.json
53
package.json
|
|
@ -1,17 +1,21 @@
|
|||
{
|
||||
"name": "oidc-auth-gcp",
|
||||
"version": "0.1.0",
|
||||
"description": "Authenticate to Google Cloud using a GitHub Actions OIDC token.",
|
||||
"main": "dist/index.js",
|
||||
"name": "@google-github-actions/auth",
|
||||
"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 src/main.ts",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"test": "mocha -r ts-node/register -t 120s 'tests/*.test.ts'"
|
||||
"build": "ncc build -m src/main.ts -o dist/main && ncc build -m src/post.ts -o dist/post",
|
||||
"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",
|
||||
"url": "https://github.com/sethvargo/oidc-auth-gcp"
|
||||
"url": "https://github.com/google-github-actions/auth"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
|
|
@ -23,23 +27,22 @@
|
|||
"author": "GoogleCloudPlatform",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.5.0"
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/http-client": "^2.2.3",
|
||||
"@google-github-actions/actions-utils": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.0",
|
||||
"@typescript-eslint/parser": "^4.31.0",
|
||||
"@zeit/ncc": "^0.22.3",
|
||||
"chai": "^4.3.4",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"husky": "^7.0.2",
|
||||
"mocha": "^9.1.1",
|
||||
"prettier": "^2.4.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.3.5"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
221
src/client.ts
221
src/client.ts
|
|
@ -1,221 +0,0 @@
|
|||
import https, { RequestOptions } from 'https';
|
||||
import { URL } from 'url';
|
||||
|
||||
/**
|
||||
* GitHubTokenParameters are the parameters to generate an OIDC token from
|
||||
* within a GitHub Action.
|
||||
*
|
||||
* @param url URL endpoint from which to request the token.
|
||||
* @param audience JWT aud value for the token.
|
||||
* @param token Temporary token provided by the environment to request the real
|
||||
* token.
|
||||
*/
|
||||
interface GitHubTokenParameters {
|
||||
url: string;
|
||||
audience: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleFederatedTokenParameters are the parameters to generate a Federated
|
||||
* Identity Token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/access-resources-oidc#exchange-token
|
||||
*
|
||||
* @param providerID Full path (including project, location, etc) to the Google
|
||||
* Cloud Workload Identity Provider.
|
||||
* @param token OIDC token to exchange for a Google Cloud federated token.
|
||||
*/
|
||||
interface GoogleFederatedTokenParameters {
|
||||
providerID: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoogleAccessTokenParameters are the parameters to generate a Google Cloud
|
||||
* access token as described in:
|
||||
*
|
||||
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
|
||||
*
|
||||
* @param token OAuth token or Federated access token with permissions to call
|
||||
* the API.
|
||||
* @param serviceAccount 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 duration.
|
||||
*/
|
||||
interface GoogleAccessTokenParameters {
|
||||
token: string;
|
||||
serviceAccount: string;
|
||||
delegates?: Array<string>;
|
||||
lifetime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
interface GoogleAccessTokenResponse {
|
||||
accessToken: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
export class Client {
|
||||
/**
|
||||
* request is a high-level helper that returns a promise from the executed
|
||||
* request.
|
||||
*/
|
||||
static request(opts: RequestOptions, data?: any): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(opts, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
|
||||
let body = '';
|
||||
res.on('data', (data) => {
|
||||
body += data;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(body);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
if (data != null) {
|
||||
req.write(data);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* githubToken invokes the given URL, appending the audience parameter, using
|
||||
* the provided token as authentication. This can only be run from inside a
|
||||
* GitHub Action.
|
||||
*/
|
||||
static async githubToken({ url, audience, token }: GitHubTokenParameters): Promise<string> {
|
||||
const requestURL = new URL(url);
|
||||
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', audience);
|
||||
requestURL.search = params.toString();
|
||||
|
||||
// Make the request.
|
||||
const opts = {
|
||||
hostname: requestURL.hostname,
|
||||
port: requestURL.port,
|
||||
path: requestURL.pathname + requestURL.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await Client.request(opts);
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['value'];
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate GitHub OIDC token via ${url} (aud: ${audience}): ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* googleFederatedToken generates a Google Cloud federated token using the
|
||||
* provided OIDC token and Workload Identity Provider.
|
||||
*/
|
||||
static async googleFederatedToken({
|
||||
providerID,
|
||||
token,
|
||||
}: GoogleFederatedTokenParameters): Promise<string> {
|
||||
const stsURL = new URL('https://sts.googleapis.com/v1/token');
|
||||
|
||||
const data = {
|
||||
audience: '//iam.googleapis.com/' + 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: token,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: stsURL.hostname,
|
||||
port: stsURL.port,
|
||||
path: stsURL.pathname + stsURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return parsed['access_token'];
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* googleAccessToken generates a Google Cloud access token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
static async googleAccessToken({
|
||||
token,
|
||||
serviceAccount,
|
||||
delegates,
|
||||
lifetime,
|
||||
}: GoogleAccessTokenParameters): Promise<GoogleAccessTokenResponse> {
|
||||
const serviceAccountID = `projects/-/serviceAccounts/${serviceAccount}`;
|
||||
const tokenURL = new URL(
|
||||
`https://iamcredentials.googleapis.com/v1/${serviceAccountID}:generateAccessToken`,
|
||||
);
|
||||
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
lifetime: lifetime,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
hostname: tokenURL.hostname,
|
||||
port: tokenURL.port,
|
||||
path: tokenURL.pathname + tokenURL.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await Client.request(opts, JSON.stringify(data));
|
||||
const parsed = JSON.parse(resp);
|
||||
return {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
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';
|
||||
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));
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
390
src/main.ts
390
src/main.ts
|
|
@ -1,79 +1,343 @@
|
|||
'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 * as core from '@actions/core';
|
||||
import { Client } from './client';
|
||||
import { join as pathjoin } from 'path';
|
||||
|
||||
/**
|
||||
* Converts a multi-line or comma-separated collection of strings into an array
|
||||
* of trimmed strings.
|
||||
*/
|
||||
function explodeStrings(input: string): Array<string> {
|
||||
if (input == null || input.length === 0) {
|
||||
return [];
|
||||
import {
|
||||
exportVariable,
|
||||
getIDToken,
|
||||
getInput,
|
||||
setFailed,
|
||||
setOutput,
|
||||
setSecret,
|
||||
} from '@actions/core';
|
||||
import {
|
||||
errorMessage,
|
||||
exactlyOneOf,
|
||||
isEmptyDir,
|
||||
isPinnedToHead,
|
||||
parseMultilineCSV,
|
||||
parseBoolean,
|
||||
parseDuration,
|
||||
pinnedToHeadWarning,
|
||||
withRetries,
|
||||
} from '@google-github-actions/actions-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 ` +
|
||||
`is being injected into the environment. By default, secrets are not ` +
|
||||
`passed to workflows triggered from forks, including Dependabot.`;
|
||||
|
||||
const oidcWarning =
|
||||
`GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ` +
|
||||
`$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely means the ` +
|
||||
`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`;
|
||||
|
||||
export async function run(logger: Logger) {
|
||||
// Warn if pinned to HEAD
|
||||
if (isPinnedToHead()) {
|
||||
logger.warning(pinnedToHeadWarning('v2'));
|
||||
}
|
||||
|
||||
const list = new Array<string>();
|
||||
for (const line of input.split(`\n`)) {
|
||||
for (const piece of line.split(',')) {
|
||||
const entry = piece.trim();
|
||||
if (entry !== '') {
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the main action, documented inline.
|
||||
*/
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
// Load configuration.
|
||||
const workloadIdentityProvider = core.getInput('workload_identity_provider', {
|
||||
required: true,
|
||||
});
|
||||
const serviceAccount = core.getInput('service_account', { required: true });
|
||||
const audience = core.getInput('audience');
|
||||
const delegates = explodeStrings(core.getInput('delegates'));
|
||||
const lifetime = core.getInput('lifetime');
|
||||
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`);
|
||||
|
||||
// Extract the GitHub Actions OIDC token.
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
if (!requestToken) {
|
||||
throw `missing ACTIONS_ID_TOKEN_REQUEST_TOKEN`;
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!requestURL) {
|
||||
throw `missing ACTIONS_ID_TOKEN_REQUEST_URL`;
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
const githubOIDCToken = await Client.githubToken({
|
||||
url: requestURL,
|
||||
token: requestToken,
|
||||
audience: audience,
|
||||
});
|
||||
core.setSecret(githubOIDCToken);
|
||||
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = await Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
// 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`);
|
||||
|
||||
// Exchange the Google Federated Token for an access token.
|
||||
const { accessToken, expiration } = await Client.googleAccessToken({
|
||||
token: googleFederatedToken,
|
||||
serviceAccount: serviceAccount,
|
||||
delegates: delegates,
|
||||
lifetime: lifetime,
|
||||
// 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,
|
||||
});
|
||||
core.setSecret(accessToken);
|
||||
core.setOutput('access_token', accessToken);
|
||||
core.setOutput('expiration', expiration);
|
||||
|
||||
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) {
|
||||
core.setFailed(`Action failed with error: ${err}`);
|
||||
const msg = errorMessage(err);
|
||||
setFailed(`google-github-actions/auth failed with: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
if (require.main === module) {
|
||||
run(new Logger());
|
||||
}
|
||||
|
|
|
|||
56
src/post.ts
Normal file
56
src/post.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// 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 { getInput, setFailed } from '@actions/core';
|
||||
|
||||
import { errorMessage, forceRemove, parseBoolean } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { Logger } from './logger';
|
||||
|
||||
export async function run(logger: Logger) {
|
||||
try {
|
||||
const createCredentials = parseBoolean(getInput('create_credentials_file'));
|
||||
if (!createCredentials) {
|
||||
logger.info(`Skipping credential cleanup - "create_credentials_file" is false.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanupCredentials = parseBoolean(getInput('cleanup_credentials'));
|
||||
if (!cleanupCredentials) {
|
||||
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
|
||||
// accidentally clean up if someone set GOOGLE_APPLICATION_CREDENTIALS or
|
||||
// another environment variable manually.
|
||||
const credentialsPath = process.env['GOOGLE_GHA_CREDS_PATH'];
|
||||
if (!credentialsPath) {
|
||||
logger.info(`Skipping credential cleanup - $GOOGLE_GHA_CREDS_PATH is not set.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the file.
|
||||
await forceRemove(credentialsPath);
|
||||
logger.info(`Removed exported credentials at "${credentialsPath}".`);
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
setFailed(`google-github-actions/auth post failed with: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
run(new Logger());
|
||||
}
|
||||
150
src/utils.ts
Normal file
150
src/utils.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// 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 {
|
||||
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
|
||||
* DWD exchange. The JWT must be signed and then exchanged with the OAuth
|
||||
* endpoints for a token.
|
||||
*
|
||||
* @param serviceAccount Email address of the service account.
|
||||
* @param subject Email address to use for impersonation.
|
||||
* @param scopes List of scopes to authorize.
|
||||
* @param lifetime Number of seconds for which the JWT should be valid.
|
||||
*/
|
||||
export function buildDomainWideDelegationJWT(
|
||||
serviceAccount: string,
|
||||
subject: string | undefined | null,
|
||||
scopes: Array<string> | undefined | null,
|
||||
lifetime: number,
|
||||
): string {
|
||||
const now = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
const body: Record<string, string | number> = {
|
||||
iss: serviceAccount,
|
||||
aud: 'https://oauth2.googleapis.com/token',
|
||||
iat: now,
|
||||
exp: now + lifetime,
|
||||
};
|
||||
if (subject && subject.trim().length > 0) {
|
||||
body.sub = subject;
|
||||
}
|
||||
if (scopes && scopes.length > 0) {
|
||||
// Yes, this is a space delimited list.
|
||||
// Not a typo, the API expects the field to be "scope" (singular).
|
||||
body.scope = scopes.join(' ');
|
||||
}
|
||||
|
||||
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:
|
||||
*
|
||||
* gha-creds-[a-z0-9]{16}.json
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* gha-creds-ef801c3bb35b52e5.json
|
||||
*
|
||||
* @return Filename
|
||||
*/
|
||||
export function generateCredentialsFilename(): string {
|
||||
return 'gha-creds-' + randomFilename(8) + '.json';
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
describe('Client', () => {
|
||||
it('todo');
|
||||
});
|
||||
109
tests/client/credentials_json_client.test.ts
Normal file
109
tests/client/credentials_json_client.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// 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 { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { join as pathjoin } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
import { randomFilename } from '@google-github-actions/actions-utils';
|
||||
|
||||
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.
|
||||
const credentialsJSON = `
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "my-project",
|
||||
"private_key_id": "1234567890abcdefghijklmnopqrstuvwxyzaabb",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCRVYIJRuxdujaX\\nUfyY9mXT1O0M3PwyT+FnPJVY+6Md7KMiPKpZRYt7okj51Ln1FLcb9mY17LzPEAxS\\nBPn1LWNpSJpmttI/D3U+bG/znf/E89ErVopYWpaynbYrb/Mu478IE9TgvnqJMlkj\\nlQbaxnZ7qhnbI5h6p/HINWfY7xBDGZM1sc2FK9KbNfEzLdW1YiK/lWAwtfM7rbiO\\nZj+LnWm2dgwZxu0h8m68qYYMywzLcV3NTe35qdAznasc1WQvJikY+N82Wu+HjsPa\\nH0fLE3gN5r+BzDYQxEQnWANgxlsHeN9mg5LAg5fyTBwTS7Ato/qQ07da0CSoS1M0\\nriYvuCzhAgMBAAECggEAAai+m9fG5B03kIMLpY5O7Rv9AM+ufb91hx6Nwkp7r4M5\\nt11vY7I96wuYJ92iBu8m4XR6fGw0Xz3gkcQ69ZCu5320hBdPrJsrqXwMhgxgoGcq\\nWuB8aJEWASi+T9hGENA++eDQFMupWV6HafzCdxd4NKAfmZ/xf1OFUu0TVpvxKlAD\\ne6Njz/5+QFdUcNioi7iGy1Qz7xdpClEWdVin8VWe3p6UsCLfHmQfPPuLXOvpBj6k\\niFu9dl93z+8vlDLoAyXSaDeYyRMBGVOBM36cICuVpxfV1s/corEZXhz3aI8mlYiQ\\n6YXTcEnllt+NTJDIL99CnYn+WBVzeIGXtr0EKAyM6QKBgQDCU6FDvU0P8qt45BDm\\nSP2V7uMoI32mjEA3plJzqqSZ9ritxFmylrOttOoTYH2FVjrKPZZsLihSjpmm+wEz\\nGfjd75eSJYAb/m7GNOqbJjqAJIbIMaHfVcH6ODT2b0Tc8v/CK0PZy/jzgt68TdtF\\no462tr8isj7yLpCGdoLq9iq4gwKBgQC/dWTGFnaI08v1uqx6derf+qikSsjlYh4L\\nDdTlI8/eaTR90PFPQ4a8LE8pmhMhkJNg87jAF5VF29sPmlpfKbOC87C2iI8uIHcn\\nu0sTdhn6SukyUSN/eeb1KSDJuxDvIgPRTZj6XMlUulADeLRnlAoWOe0tu/wqpse6\\nB0Qu2oAfywKBgQCMWukESyro1OZit585JQj7jQJG0HOFopETYK722g5vIdM7trDu\\nm4iFc0EJ48xlTOXDgv4tfp0jG9oA0BSKuzyT1+RK64j/LyMFR90XWGIyga9T0v1O\\nmNs1BfnC8JT1XRG7RZKJMZjLEQAdU8KHJt4CPDYLMmDifR1n8RsX59rtTwKBgQCS\\nnAmsKn1gb5cqt2Tmba+LDj3feSj3hjftTQ0u3kqKTNOWWM7AXLwrEl8YQ1TNChHh\\nVyCtcCGtmhrYiuETKDK/X259iHrj3paABUsLPw/Le1uxXTKqpiV2rKTf9XCVPd3g\\ng+RWK4E8cWNeFStIebNzq630rJP/8TDWQkQzALzGGwKBgQC5bnlmipIGhtX2pP92\\niBM8fJC7QXbyYyamriyFjC3o250hHy7mZZG7bd0bH3gw0NdC+OZIBNv7AoNhjsvP\\nuE0Qp/vQXpgHEeYFyfWn6PyHGzqKLFMZ/+iCTuy8Iebs1p5DZY8RMXpx4tv6NfRy\\nbxHUjlOgP7xmXM+OZpNymFlRkg==\\n-----END PRIVATE KEY-----\\n",
|
||||
"client_email": "my-service-account@my-project.iam.gserviceaccount.com",
|
||||
"client_id": "123456789098765432101",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/my-service-account%40my-project.iam.gserviceaccount.com"
|
||||
}
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
await suite.test('handles base64', async () => {
|
||||
await assert.rejects(async () => {
|
||||
new ServiceAccountKeyClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
serviceAccountKey: 'base64',
|
||||
});
|
||||
}, 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);
|
||||
});
|
||||
});
|
||||
104
tests/client/workload_identity_client.test.ts
Normal file
104
tests/client/workload_identity_client.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// 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 { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { tmpdir } from 'os';
|
||||
import { join as pathjoin } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import { randomFilename } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { NullLogger } from '../../src/logger';
|
||||
import { WorkloadIdentityFederationClient } from '../../src/client/workload_identity_federation';
|
||||
|
||||
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',
|
||||
|
||||
githubOIDCToken: 'my-token',
|
||||
githubOIDCTokenRequestURL: 'https://example.com/',
|
||||
githubOIDCTokenRequestToken: 'token',
|
||||
githubOIDCTokenAudience: 'my-aud',
|
||||
workloadIdentityProviderName: 'my-provider',
|
||||
});
|
||||
|
||||
const exp = {
|
||||
audience: '//iam.googleapis.com/my-provider',
|
||||
credential_source: {
|
||||
format: {
|
||||
subject_token_field_name: 'value',
|
||||
type: 'json',
|
||||
},
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
url: 'https://example.com/?audience=my-aud',
|
||||
},
|
||||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
token_url: 'https://sts.googleapis.com/v1/token',
|
||||
type: 'external_account',
|
||||
};
|
||||
|
||||
const pth = await client.createCredentialsFile(outputFile);
|
||||
const data = readFileSync(pth);
|
||||
const got = JSON.parse(data.toString('utf8'));
|
||||
|
||||
assert.deepStrictEqual(got, exp);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
171
tests/utils.test.ts
Normal file
171
tests/utils.test.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// 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 { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import {
|
||||
buildDomainWideDelegationJWT,
|
||||
computeProjectID,
|
||||
computeServiceAccountEmail,
|
||||
generateCredentialsFilename,
|
||||
projectIDFromServiceAccountEmail,
|
||||
} from '../src/utils';
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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);
|
||||
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(' '));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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,15 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"alwaysStrict": true,
|
||||
"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/"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue