Compare commits

...

251 commits

Author SHA1 Message Date
Seth Vargo
fc2174804b
Update README with correct Node version (#515)
Closes https://github.com/google-github-actions/auth/issues/514

Signed-off-by: Seth Vargo <seth@sethvargo.com>
2025-09-03 09:38:45 -04:00
Seth Vargo
0dfce0c0f8
Update README to reference v3 (#511) 2025-08-28 14:55:07 -04:00
Google GitHub Actions Bot
7c6bc770da
Release: v3.0.0 (#510)
## What's Changed
* Bump to Node 24 and remove old parameters by @sethvargo in
https://github.com/google-github-actions/auth/pull/508
* Remove hacky script by @sethvargo in
https://github.com/google-github-actions/auth/pull/509


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.13...v3.0.0
2025-08-28 18:51:40 +00:00
Seth Vargo
42e4997ee3
Remove hacky script (#509) 2025-08-28 14:44:31 -04:00
Seth Vargo
5ea4dc1147
Bump to Node 24 and remove old parameters (#508) 2025-08-28 14:39:57 -04:00
Google GitHub Actions Bot
c200f3691d
Release: v2.1.13 (#507)
## What's Changed
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/506


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.12...v2.1.13
2025-08-28 18:27:28 +00:00
Seth Vargo
3a53be7e7c
Update deps (#506) 2025-08-28 14:25:15 -04:00
Google GitHub Actions Bot
b7593ed2ef
Release: v2.1.12 (#503)
## What's Changed
* Add retries for getIDToken by @sethvargo in
https://github.com/google-github-actions/auth/pull/502


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.11...v2.1.12
2025-08-01 15:15:43 +00:00
Seth Vargo
c1ee334b4f
Add retries for getIDToken (#502)
Closes #496
2025-08-01 10:44:21 -04:00
Google GitHub Actions Bot
140bb5113f
Release: v2.1.11 (#501)
## What's Changed
* Update troubleshooting docs for Python by @sethvargo in
https://github.com/google-github-actions/auth/pull/488
* Add linters by @sethvargo in
https://github.com/google-github-actions/auth/pull/499
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/500


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.10...v2.1.11
2025-07-18 21:17:15 +00:00
Seth Vargo
ab3132e2ad
Update deps (#500) 2025-07-18 17:13:12 -04:00
Seth Vargo
25b96bac99
Add linters (#499) 2025-07-18 01:13:31 +00:00
Seth Vargo
0920706a19
Update troubleshooting docs for Python (#488)
Closes https://github.com/google-github-actions/auth/pull/487

---------

Co-authored-by: Abhi Srivastava <bits.abhi@gmail.com>
2025-06-02 10:48:14 -04:00
Google GitHub Actions Bot
ba79af0395
Release: v2.1.10 (#484)
## What's Changed
* Declare workflow permissions by @sethvargo in
https://github.com/google-github-actions/auth/pull/482
* Document that the OIDC token expires in 5min by @sethvargo in
https://github.com/google-github-actions/auth/pull/483


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.9...v2.1.10
2025-04-25 09:48:31 -04:00
Seth Vargo
bfaa66bd66
Document that the OIDC token expires in 5min (#483) 2025-04-25 08:43:32 -04:00
Seth Vargo
d0822ad9bf
Declare workflow permissions (#482) 2025-04-25 08:42:57 -04:00
Google GitHub Actions Bot
7b53cdc2a3
Release: v2.1.9 (#480)
## What's Changed
* Use our custom boolean parsing by @sethvargo in
https://github.com/google-github-actions/auth/pull/478
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/479


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.8...v2.1.9
2025-04-24 17:48:14 +00:00
Seth Vargo
a9cfddf5d2
Update deps (#479) 2025-04-24 13:44:06 -04:00
Seth Vargo
b011f3988e
Use our custom boolean parsing (#478)
Fixes GH-477
2025-04-24 11:53:29 -04:00
Google GitHub Actions Bot
71f986410d
Release: v2.1.8 (#467)
## What's Changed
* Update TROUBLESHOOTING.md by @sethvargo in
https://github.com/google-github-actions/auth/pull/457
* fix: add runs-on to README.md example by @lbarthon in
https://github.com/google-github-actions/auth/pull/460
* security: bump undici from 5.28.4 to 5.28.5 in the npm_and_yarn group
by @dependabot in https://github.com/google-github-actions/auth/pull/463
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/466

## New Contributors
* @lbarthon made their first contribution in
https://github.com/google-github-actions/auth/pull/460

**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.7...v2.1.8
2025-02-01 14:16:56 +00:00
Seth Vargo
0cd8f2e4e2
Update deps (#466) 2025-02-01 08:49:34 -05:00
dependabot[bot]
332e0ba72f
security: bump undici from 5.28.4 to 5.28.5 in the npm_and_yarn group (#463)
Bumps the npm_and_yarn group with 1 update:
[undici](https://github.com/nodejs/undici).

Updates `undici` from 5.28.4 to 5.28.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodejs/undici/releases">undici's
releases</a>.</em></p>
<blockquote>
<h2>v5.28.5</h2>
<h1>⚠️ Security Release ⚠️</h1>
<p>Fixes CVE CVE-2025-22150 <a
href="https://github.com/nodejs/undici/security/advisories/GHSA-c76h-2ccp-4975">https://github.com/nodejs/undici/security/advisories/GHSA-c76h-2ccp-4975</a>
(embargoed until 22-01-2025).</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nodejs/undici/compare/v5.28.4...v5.28.5">https://github.com/nodejs/undici/compare/v5.28.4...v5.28.5</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6139ed2e0c"><code>6139ed2</code></a>
Bumped v5.28.5</li>
<li><a
href="711e207727"><code>711e207</code></a>
Backport of c2d78cd</li>
<li>See full diff in <a
href="https://github.com/nodejs/undici/compare/v5.28.4...v5.28.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=undici&package-manager=npm_and_yarn&previous-version=5.28.4&new-version=5.28.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/google-github-actions/auth/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 18:18:58 -05:00
Louis
28d44ba259
fix: add runs-on to README.md example (#460)
This is needed in order for this example to "work". The runner that
needs to be picked is specified above, but it's still handy if we can
simply copy / paste from the README.

<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->
2025-01-23 11:44:32 -05:00
Seth Vargo
83354cacbb
Update TROUBLESHOOTING.md (#457)
Fix a bunch of issues since GitHub apparently dropped support for
special callouts.

Refs https://github.com/google-github-actions/auth/issues/455

Signed-off-by: Seth Vargo <seth@sethvargo.com>
2024-12-02 09:20:26 -05:00
Google GitHub Actions Bot
6fc4af4b14
Release: v2.1.7 (#453)
## What's Changed
* fix: update relase workflows by @verbanicm in
https://github.com/google-github-actions/auth/pull/452


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.6...212f83afe868cc88aa354e1efbf778eff05d970a
2024-10-30 18:53:27 +00:00
Mike Verbanic
212f83afe8
fix: update relase workflows (#452)
<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->
2024-10-30 14:26:20 -04:00
Google GitHub Actions Bot
8254fb75a3
Release: v2.1.6 (#449)
## What's Changed
* Recommend `gcloud storage` over `gsutil` by @sethvargo in
https://github.com/google-github-actions/auth/pull/438
* Add missing log line by @sethvargo in
https://github.com/google-github-actions/auth/pull/448


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.5...d1b27fe5c4d3d1e3dc8a68e8fc94cc4b24009c24
2024-10-01 19:37:05 +00:00
Seth Vargo
d1b27fe5c4
Add missing log line (#448) 2024-10-01 14:04:12 +00:00
Seth Vargo
c8788cc4c5
Recommend gcloud storage over gsutil (#438)
Closes #404
2024-08-21 17:00:27 +00:00
Google GitHub Actions Bot
62cf5bd3e4
Release: v2.1.5 (#437)
## What's Changed
* Document ID Token lifetimes by @sethvargo in
https://github.com/google-github-actions/auth/pull/433
* fix !project_id error message typo by @seth-acuitymd in
https://github.com/google-github-actions/auth/pull/435
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/436

## New Contributors
* @seth-acuitymd made their first contribution in
https://github.com/google-github-actions/auth/pull/435

**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.4...0a94a84ba5475d3020c3df611c610b909d57bbb9
2024-08-21 02:43:58 +00:00
Seth Vargo
0a94a84ba5
Update deps (#436) 2024-08-20 20:58:29 -04:00
Seth McCombs
699582eeaf
fix !project_id error message typo (#435)
<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->

Small change! This error message seems like it's missing a word, so
based on the comment on line 202, I changed it
from
> `⚠️ Failed to a project ID from the given inputs.`

to

> `⚠️ Failed to compute a project ID from the given inputs`
2024-08-20 15:21:32 -07:00
Seth Vargo
6384b341b7
Document ID Token lifetimes (#433)
Closes https://github.com/google-github-actions/auth/issues/432
2024-08-07 01:28:34 +00:00
Google GitHub Actions Bot
f112390a2d
Release: v2.1.4 (#431)
## What's Changed
* security: bump braces from 3.0.2 to 3.0.3 in the npm_and_yarn group by
@dependabot in https://github.com/google-github-actions/auth/pull/420
* Update spelling and workflow versions by @sethvargo in
https://github.com/google-github-actions/auth/pull/422
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/430


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.3...984b9cfee9afc210d62d5d59c77fdf75553ada22
2024-08-06 01:27:11 +00:00
Seth Vargo
984b9cfee9
Update deps (#430) 2024-08-05 16:15:15 -04:00
Seth Vargo
5e210ff4ed
Update spelling and workflow versions (#422)
Closes #421

---------

Signed-off-by: JGStew <james@jgstew.com>
Signed-off-by: JGStew <jamesgstewart2@gmail.com>
Co-authored-by: JGStew <james@jgstew.com>
Co-authored-by: JGStew <jamesgstewart2@gmail.com>
2024-06-21 11:57:04 -04:00
dependabot[bot]
49ae1e804e
security: bump braces from 3.0.2 to 3.0.3 in the npm_and_yarn group (#420)
Bumps the npm_and_yarn group with 1 update:
[braces](https://github.com/micromatch/braces).

Updates `braces` from 3.0.2 to 3.0.3
<details>
<summary>Commits</summary>
<ul>
<li><a
href="74b2db2938"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="88f1429a0f"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="415d660c30"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="190510f79d"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="716eb9f12d"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="a5851e57f4"><code>a5851e5</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/braces/issues/37">#37</a>
from coderaiser/fix/vulnerability</li>
<li><a
href="2092bd1fb1"><code>2092bd1</code></a>
feature: braces: add maxSymbols (<a
href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a
href="9f5b4cf473"><code>9f5b4cf</code></a>
fix: vulnerability (<a
href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a
href="98414f9f1f"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="665ab5d561"><code>665ab5d</code></a>
update keepEscaping doc (<a
href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=braces&package-manager=npm_and_yarn&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/google-github-actions/auth/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 14:12:52 +00:00
Google GitHub Actions Bot
71fee32a0b
Release: v2.1.3 (#414)
## What's Changed
* Security considerations: ids are strings, not integers by @ewjoachim
in https://github.com/google-github-actions/auth/pull/400
* security: bump undici from 5.28.3 to 5.28.4 by @dependabot in
https://github.com/google-github-actions/auth/pull/405
* Fix typo by @sethvargo in
https://github.com/google-github-actions/auth/pull/408
* Switch to using universe helpers by @sethvargo in
https://github.com/google-github-actions/auth/pull/410
* Add request_reason for plumbing though user-supplied audit information
by @sethvargo in https://github.com/google-github-actions/auth/pull/413

## New Contributors
* @ewjoachim made their first contribution in
https://github.com/google-github-actions/auth/pull/400

**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.2...e0122d6a976dd6794fe6e866adfcb3c11f828b36
2024-05-14 13:57:49 -04:00
Seth Vargo
e0122d6a97
Add request_reason for plumbing though user-supplied audit information (#413)
Fixes https://github.com/google-github-actions/auth/issues/412
2024-05-14 16:46:35 +00:00
Seth Vargo
34baaec3f3
Switch to using universe helpers (#410) 2024-05-04 12:05:55 -04:00
Seth Vargo
8d44d59719
Fix typo (#408)
Fixes #407
2024-04-16 21:39:19 +00:00
dependabot[bot]
d176447fc7
security: bump undici from 5.28.3 to 5.28.4 (#405)
Bumps [undici](https://github.com/nodejs/undici) from 5.28.3 to 5.28.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodejs/undici/releases">undici's
releases</a>.</em></p>
<blockquote>
<h2>v5.28.4</h2>
<h2>⚠️ Security Release ⚠️</h2>
<ul>
<li>Fixes <a
href="https://github.com/nodejs/undici/security/advisories/GHSA-m4v8-wqvr-p9f7">https://github.com/nodejs/undici/security/advisories/GHSA-m4v8-wqvr-p9f7</a>
CVE-2024-30260</li>
<li>Fixes <a
href="https://github.com/nodejs/undici/security/advisories/GHSA-9qxr-qj54-h672">https://github.com/nodejs/undici/security/advisories/GHSA-9qxr-qj54-h672</a>
CVE-2024-30261</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nodejs/undici/compare/v5.28.3...v5.28.4">https://github.com/nodejs/undici/compare/v5.28.3...v5.28.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fb98306907"><code>fb98306</code></a>
Bumped v5.28.4</li>
<li><a
href="2b39440bd9"><code>2b39440</code></a>
Merge pull request from GHSA-9qxr-qj54-h672</li>
<li><a
href="64e3402da4"><code>64e3402</code></a>
Merge pull request from GHSA-m4v8-wqvr-p9f7</li>
<li><a
href="723c4e7280"><code>723c4e7</code></a>
Revert &quot;build(deps-dev): bump formdata-node from 4.4.1 to 6.0.3 (<a
href="https://redirect.github.com/nodejs/undici/issues/2389">#2389</a>)&quot;</li>
<li><a
href="0e9d54b2c2"><code>0e9d54b</code></a>
skip failing test due to Node.js changes</li>
<li>See full diff in <a
href="https://github.com/nodejs/undici/compare/v5.28.3...v5.28.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=undici&package-manager=npm_and_yarn&previous-version=5.28.3&new-version=5.28.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/google-github-actions/auth/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 16:04:39 -04:00
Joachim Jablon
33e827c6cc
Security considerations: ids are strings, not integers (#400)
Fix doc regarding using assertion on IDs rather than names. They refer
to IDs as integers, where those are actually strings, so we need to
quote them.

I lost too many hours on this to let anyone else experience the same
issue :D

Signed-off-by: Joachim Jablon <ewjoachim@gmail.com>
2024-03-12 11:21:38 -04:00
Google GitHub Actions Bot
55bd3a7c6e
Release: v2.1.2 (#399)
## What's Changed
* Remove documentation on retries (deprecated) by @sethvargo in
https://github.com/google-github-actions/auth/pull/392
* Add security considerations for Attribute Conditions by @sethvargo in
https://github.com/google-github-actions/auth/pull/393
* security: bump undici from 5.28.2 to 5.28.3 by @dependabot in
https://github.com/google-github-actions/auth/pull/394
* Reduce warnings to info level with a warning icon by @sethvargo in
https://github.com/google-github-actions/auth/pull/397

## New Contributors
* @dependabot made their first contribution in
https://github.com/google-github-actions/auth/pull/394

**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.1...bf02f20c66a26ba12adc45fa735c39f496ab04df
2024-02-25 19:44:23 +00:00
Seth Vargo
bf02f20c66
Reduce warnings to info level with a warning icon (#397)
With direct WIF, there are now many legitimate use cases for not
specifying a project_id or having a local checkout.
2024-02-25 14:29:20 -05:00
dependabot[bot]
51342a1a91
security: bump undici from 5.28.2 to 5.28.3 (#394)
Bumps [undici](https://github.com/nodejs/undici) from 5.28.2 to 5.28.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodejs/undici/releases">undici's
releases</a>.</em></p>
<blockquote>
<h2>v5.28.3</h2>
<h2>⚠️ Security Release ⚠️</h2>
<p>Fixes:</p>
<ul>
<li><a
href="https://github.com/nodejs/undici/security/advisories/GHSA-3787-6prv-h9w3">CVE-2024-24758
Proxy-Authorization header not cleared on cross-origin redirect in
fetch</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nodejs/undici/compare/v5.28.2...v5.28.3">https://github.com/nodejs/undici/compare/v5.28.2...v5.28.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e71cb4c88f"><code>e71cb4c</code></a>
Bumped v5.28.3</li>
<li><a
href="20c65b89f4"><code>20c65b8</code></a>
Fix tests for Node.js v20.11.0 (<a
href="https://redirect.github.com/nodejs/undici/issues/2618">#2618</a>)</li>
<li><a
href="8ec52cde66"><code>8ec52cd</code></a>
Fix tests for Node.js v21 (<a
href="https://redirect.github.com/nodejs/undici/issues/2609">#2609</a>)</li>
<li><a
href="d3aa574b12"><code>d3aa574</code></a>
Merge pull request from GHSA-3787-6prv-h9w3</li>
<li>See full diff in <a
href="https://github.com/nodejs/undici/compare/v5.28.2...v5.28.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=undici&package-manager=npm_and_yarn&previous-version=5.28.2&new-version=5.28.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/google-github-actions/auth/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 15:07:59 -05:00
Seth Vargo
ee1c1b641f
Add security considerations for Attribute Conditions (#393) 2024-02-07 08:19:20 -05:00
Seth Vargo
ec485ac236
Remove documentation on retries (deprecated) (#392) 2024-02-06 16:34:44 +00:00
Google GitHub Actions Bot
a6e2e39c0a
Release: v2.1.1 (#390)
## What's Changed
* Remove retry logic by @sethvargo in
https://github.com/google-github-actions/auth/pull/389
* Use an OAuth 2.0 access token for Domain-Wide Delegation by @sethvargo
in https://github.com/google-github-actions/auth/pull/388


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.1.0...b4f4057a105e0aaddb54a56ce4953ddf3fa4e1ea
2024-02-05 16:33:46 +00:00
Seth Vargo
b4f4057a10
Use an OAuth 2.0 access token for Domain-Wide Delegation (#388)
Fixes https://github.com/google-github-actions/auth/issues/387
2024-02-05 11:27:46 -05:00
Seth Vargo
39c96a3f1d
Remove retry logic (#389)
The retries make debugging strictly more complex, and it's not clear
that retrying provides actual value. If we need retries in the future,
we should push them down into the per-API level.
2024-02-02 21:10:46 +00:00
Google GitHub Actions Bot
5a50e58116
Release: v2.1.0 (#385)
## What's Changed
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/384


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.0.1...aaf2e69f9516827f245e0f3568ac8b4abf6c1d6d
2024-01-23 02:09:45 +00:00
Seth Vargo
aaf2e69f95
Update deps (#384) 2024-01-22 21:04:58 -05:00
Google GitHub Actions Bot
f6de81663f
Release: v2.0.1 (#382)
## What's Changed
* Trigger release on pushes to release branches by @sethvargo in
https://github.com/google-github-actions/auth/pull/358
* Fix a small docs issue by @sethvargo in
https://github.com/google-github-actions/auth/pull/359
* Remove broken markdown links by @sethvargo in
https://github.com/google-github-actions/auth/pull/362
* Document that project_id might be required by @sethvargo in
https://github.com/google-github-actions/auth/pull/367
* Update README and CI to use latest version by @sethvargo in
https://github.com/google-github-actions/auth/pull/365
* Add service_account to WIF through SA example by @sethvargo in
https://github.com/google-github-actions/auth/pull/369
* Use new markdown syntax for alerts by @sethvargo in
https://github.com/google-github-actions/auth/pull/371
* Note .dockerignore in the exclusion for credentials by @sethvargo in
https://github.com/google-github-actions/auth/pull/376
* Support newline-separated inputs for delegates and access_token_scopes
by @sethvargo in https://github.com/google-github-actions/auth/pull/381


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v2.0.0...a57dd046551728ea4f537891f7c0b7758f71e77b
2024-01-08 17:39:31 -05:00
Seth Vargo
a57dd04655
Support newline-separated inputs for delegates and access_token_scopes (#381)
Fixes #380
2024-01-08 17:24:50 -05:00
Seth Vargo
fb74905737
Note .dockerignore in the exclusion for credentials (#376)
Closes https://github.com/google-github-actions/auth/pull/375

(Opening as me to trigger CI)

---------

Signed-off-by: Andrew Howden <hello@andrewhowden.com>
Signed-off-by: Seth Vargo <seth@sethvargo.com>
Co-authored-by: Andrew Howden <hello@andrewhowden.com>
2024-01-04 12:25:30 -05:00
Seth Vargo
56562ddf6a
Use new markdown syntax for alerts (#371) 2023-12-18 14:06:39 -05:00
Seth Vargo
5f7afaa53a
Add service_account to WIF through SA example (#369)
Refs https://github.com/google-github-actions/auth/issues/368
2023-12-13 23:17:38 -05:00
Seth Vargo
82c1c583d7
Update README and CI to use latest version (#365) 2023-12-13 10:28:29 -05:00
Seth Vargo
5e5db2b28b
Document that project_id might be required (#367) 2023-12-13 10:28:08 -05:00
Seth Vargo
b372ba41cf
Remove broken markdown links (#362) 2023-12-04 08:47:12 -05:00
Seth Vargo
042a3056d6
Fix a small docs issue (#359) 2023-11-29 15:46:17 +00:00
Seth Vargo
68ea0b8bd6
Trigger release on pushes to release branches (#358) 2023-11-28 22:49:37 -05:00
Google GitHub Actions Bot
67e9c72af6
Release: v2.0.0 (#355)
## What's Changed
* Add support for Direct Workload Identity auth by @sethvargo in
https://github.com/google-github-actions/auth/pull/348
* Add protection for release branches by @sethvargo in
https://github.com/google-github-actions/auth/pull/351
* Make auth universe-aware by @sethvargo in
https://github.com/google-github-actions/auth/pull/352
* Fix some examples to include project_id by @sethvargo in
https://github.com/google-github-actions/auth/pull/353


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v1.2.0...0a2edc185b2c194b4f6bcebc7a8f52ebce97cd91
2023-11-28 22:37:53 -05:00
Seth Vargo
0a2edc185b
Fix some examples to include project_id (#353) 2023-11-28 22:07:21 -05:00
Seth Vargo
7c4e01fd00
Make auth universe-aware (#352)
This adds support for making the action "universe" aware, so it will be
usable for TPC and GDCH.
2023-11-28 21:59:39 -05:00
Seth Vargo
097d292c04
Add protection for release branches (#351) 2023-11-28 10:52:27 -05:00
Seth Vargo
fe9207673e
Add support for Direct Workload Identity auth (#348)
This adds a new authentication mode, Direct Workload Identity
Federation. This new mode permits authenticating to Google Cloud
directly using the GitHub Actions OIDC token instead of proxying through
a Google Cloud Service Account.
2023-11-28 10:41:10 -05:00
Google GitHub Actions Bot
f105ef0cdb
Release: v1.2.0 (#346)
## What's Changed
* Update deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/312
* dependabot: only do security updates by @sethvargo in
https://github.com/google-github-actions/auth/pull/320
* update all deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/321
* Update README.md by @mattseymour in
https://github.com/google-github-actions/auth/pull/332
* Update deps to Node 20 by @sethvargo in
https://github.com/google-github-actions/auth/pull/339
* Drop chai and mocha deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/342
* Switch to more resilient testing by @sethvargo in
https://github.com/google-github-actions/auth/pull/343
* Build latest versions by @sethvargo in
https://github.com/google-github-actions/auth/pull/345

## New Contributors
* @mattseymour made their first contribution in
https://github.com/google-github-actions/auth/pull/332

**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v1.1.1...cf96593e466c3b0a34086310cd29fe5aa3f49ece
2023-11-20 18:38:48 +00:00
Seth Vargo
cf96593e46
Build latest versions (#345)
<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->
2023-11-20 13:23:18 -05:00
Seth Vargo
292d120a9f
Switch to more resilient testing (#343) 2023-11-15 22:27:04 -05:00
Seth Vargo
04900d1e97
Drop chai and mocha deps (#342) 2023-11-15 08:33:15 -05:00
Seth Vargo
43a59886fc
Update deps (#339) 2023-11-08 06:22:12 -08:00
Matt Seymour
e607103ba4
Update README.md (#332)
Update code sample to use latest github action/checkout@v4.

Signed-off-by: Matt Seymour <mattaseymour@gmail.com>
2023-10-04 03:02:29 -04:00
Seth Vargo
87b651ab4a
update all deps (#321) 2023-07-11 12:37:56 -04:00
Seth Vargo
8738ec4f61
dependabot: only do security updates (#320) 2023-07-11 11:09:54 -04:00
Seth Vargo
ee81b78321
Update deps (#312)
Closes https://github.com/google-github-actions/auth/pull/304
2023-05-29 11:21:36 -04:00
Google GitHub Actions Bot
35b0e87d16
Release: v1.1.1 (#306)
## What's Changed
* Add support info by @sethvargo in
https://github.com/google-github-actions/auth/pull/303
* chore: update dependencies (automated) by @verbanicm in
https://github.com/google-github-actions/auth/pull/305


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v1.1.0...24b8285e35a6e4f8d1b395a5191e2d7682ec64b8
2023-05-08 14:19:11 -04:00
Mike Verbanic
24b8285e35
chore: update dependencies (automated) (#305) 2023-05-08 11:32:57 -04:00
Seth Vargo
286fc8e4ba
Add support info (#303) 2023-05-04 11:44:07 -07:00
Google GitHub Actions Bot
e8df18b60c
Release: v1.1.0 (#297)
## What's Changed
* fix: update doc versions by @verbanicm in
https://github.com/google-github-actions/auth/pull/240
* Only emit a warning if the envvar has changed by @sethvargo in
https://github.com/google-github-actions/auth/pull/245
* Update CI and deps by @sethvargo in
https://github.com/google-github-actions/auth/pull/248
* Document possible issues with org policies by @sethvargo in
https://github.com/google-github-actions/auth/pull/258
* Updated troubleshooting to add permissions example by @bseib in
https://github.com/google-github-actions/auth/pull/262
* Note that Firebase Admin Node.js SDK doesn't support WLIF by
@kevinthecheung in
https://github.com/google-github-actions/auth/pull/268
* chore: update dependencies (automated) by @verbanicm in
https://github.com/google-github-actions/auth/pull/274
* Document admission for all repos of an owner by @djbrown in
https://github.com/google-github-actions/auth/pull/279
* Switch to pull non-secret values from env by @sethvargo in
https://github.com/google-github-actions/auth/pull/288
* Emit a diff of each environment variable by @sethvargo in
https://github.com/google-github-actions/auth/pull/296
* Enable default retries of 3 retry attempts at 250ms backoff by
@sethvargo in https://github.com/google-github-actions/auth/pull/294

## New Contributors
* @bseib made their first contribution in
https://github.com/google-github-actions/auth/pull/262
* @kevinthecheung made their first contribution in
https://github.com/google-github-actions/auth/pull/268
* @djbrown made their first contribution in
https://github.com/google-github-actions/auth/pull/279

**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v1.0.0...41d8fa4aaad5b21acb962af3a5112b0d1bd9cb1d
2023-04-17 23:02:56 +00:00
Seth Vargo
41d8fa4aaa
Enable default retries of 3 retry attempts at 250ms backoff (#294)
This changes the default behavior to enable retries. Retry logic has
existed for awhile, but required direct user action to enable. This
enables 3 retries with a fibonacci backoff of 250ms by default to handle
transient communication errors with upstream APIs.
2023-04-17 22:57:11 +00:00
Seth Vargo
14b54cc192
Emit a diff of each environment variable (#296)
Fixes https://github.com/google-github-actions/auth/issues/295
2023-04-17 22:52:00 +00:00
Seth Vargo
430ae13d31
Switch to pull non-secret values from env (#288) 2023-03-24 16:43:00 -04:00
Seth Vargo
f8751d9c29
Switch back to v0 2023-03-24 12:54:38 -04:00
Seth Vargo
7004cc6280
Temporarily use @main to test releases 2023-03-24 12:16:33 -04:00
Daniel Brown
5431d4afba
Document admission for all repos of an owner (#279)
I really struggleded several days with this and thankfully i found
https://github.com/google-github-actions/auth/issues/77#issuecomment-990371420
big thanks to @sethvargo ❤
as @dobromyslov already said, this should be documented
so I went ahead and created added a paragrah for this use case

---------

Signed-off-by: Daniel Brown <djbrown@users.noreply.github.com>
2023-03-24 10:24:13 -04:00
Mike Verbanic
b0a71801d2
chore: update dependencies (automated) (#274) 2023-03-17 10:31:40 -04:00
Kevin Cheung
126054d1a9
Note that Firebase Admin Node.js SDK doesn't support WLIF (#268)
<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->

---------

Signed-off-by: Kevin Cheung <kevinthecheung@users.noreply.github.com>
2023-03-07 19:03:11 -05:00
Broc Seib
483e21d723
Updated troubleshooting to add permissions example (#262)
<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->

Added another case that can cause `auth` to fail, helpful to users on
day one of using the tool and trying to understand what's happening.

Fixes
https://github.com/google-github-actions/auth/issues/260#issuecomment-1419662751
2023-02-21 09:06:08 -05:00
Seth Vargo
1475a55569
Document possible issues with org policies (#258)
Fixes GH-257
2023-01-24 09:02:03 -05:00
Seth Vargo
d4421b6014
Update CI and deps (#248) 2022-12-09 21:21:40 -05:00
Seth Vargo
46affe9814
Small README tweak 2022-12-06 14:41:49 -05:00
Seth Vargo
23620afd0f
Only emit a warning if the envvar has changed (#245) 2022-11-26 09:00:48 -05:00
Seth Vargo
400b51b0c2
Point to OIDC debugger 2022-11-18 10:13:17 -05:00
Seth Vargo
7c1d54bea7
Tweak some documentation around troubleshooting 2022-11-17 09:33:51 -05:00
Seth Vargo
abd9c242f6
Add troubleshooting for enterprise installations 2022-11-11 09:05:21 -05:00
Mike Verbanic
dcb0567d1a
fix: update doc versions (#240)
<!--
Thank you for proposing a pull request! Please note that SOME TESTS WILL
LIKELY FAIL due to how GitHub exposes secrets in Pull Requests from
forks.
Someone from the team will review your Pull Request and respond.

Please describe your change and any implementation details below.
-->
2022-11-09 09:11:19 -05:00
Google GitHub Actions Bot
ef5d53e30b
Release: v1.0.0 (#239)
## What's Changed
* Update dependencies and tweak README by @sethvargo in
https://github.com/google-github-actions/auth/pull/237
* Switch to auth@v1 by @sethvargo in
https://github.com/google-github-actions/auth/pull/238


**Full Changelog**:
https://github.com/google-github-actions/auth/compare/v0.8.3...d419231eb96d10101085218406fb847f866ed776
2022-11-08 17:31:50 +00:00
Seth Vargo
d419231eb9
Fix one 2022-11-08 12:01:21 -05:00
Seth Vargo
9cf496b575
Switch to auth@v1 (#238)
There's a chicken-and-egg problem here where we want to have the docs
reflect v1 (so the github marketplace shows the right thing), but we
can't cut v1 until the docs are updated.
2022-11-08 12:00:36 -05:00
Seth Vargo
44ae44c257
Update dependencies and tweak README (#237) 2022-11-08 11:20:52 -05:00
Seth Vargo
60569fa4c7
Clarify docs 2022-11-04 15:12:05 -04:00
Seth Vargo
7e38fb10d3
Add capitalization to troubleshooting
Fixes #234
2022-10-24 20:49:23 -04:00
Google GitHub Actions Bot
c4799db911
Release: v0.8.3 (#232) 2022-10-14 20:30:25 +00:00
Mike Verbanic
deb4ef6f17
chore: update dependencies (automated) (#231) 2022-10-14 16:17:51 -04:00
Google GitHub Actions Bot
2671692c37
Release: v0.8.2 (#229) 2022-10-12 13:34:13 -04:00
Seth Vargo
5ed7985d75
Update deps to fix setOutput issue (#228) 2022-10-12 10:25:39 -07:00
Google GitHub Actions Bot
dac4e13deb
Release: v0.8.1 (#219) 2022-08-31 16:28:27 -07:00
Seth Vargo
c4e906d161
Add proxy support (#218) 2022-08-31 19:13:51 -04:00
Seth Vargo
246692c248
chore: update deps (#215) 2022-08-25 09:02:06 -04:00
Seth Vargo
c06176fb62
Document that WIF is supported for gsutil and bq now (#213) 2022-08-17 16:26:52 -07:00
Seth Vargo
2b207063da
Delete CONTRIBUTING.md (#209)
Delegate to .github organization repo

Signed-off-by: Seth Vargo <seth@sethvargo.com>
2022-08-09 14:02:07 -05:00
Seth Vargo
acd8a2b487
Document how GitHub secrets work (#208)
Fixes #207
2022-08-08 08:46:38 -07:00
Daz Wilkin
8609d2cff4
r/an/and (#205)
Signed-off-by: Daz Wilkin <DazWilkin@users.noreply.github.com>
2022-07-18 11:56:21 -04:00
Seth Vargo
d394832e06
docs: clarify that token properties don't extend beyond the action (#204) 2022-07-06 12:12:08 -04:00
Seth Vargo
b792fe33f8
chore: update deps (#199) 2022-06-21 10:10:01 -05:00
Mike Verbanic
02b3962042
$'fix release workflow files' (#198) 2022-06-16 16:14:54 -04:00
Seth Vargo
157280cef2
Minor touch-ups on comment 2022-05-30 10:31:52 -04:00
Seth Vargo
a771ff81e2
Run on all new issues 2022-05-30 10:29:19 -04:00
Seth Vargo
d786187a9e
Try just on labels 2022-05-30 10:25:52 -04:00
Seth Vargo
1336d3b920
Double quotes 2022-05-29 13:14:12 -04:00
Seth Vargo
7c65c4311a
Quotes? 2022-05-29 13:12:51 -04:00
Seth Vargo
a1109f2c8e
Mention the user 2022-05-29 13:11:58 -04:00
Seth Vargo
f899132198
Add workflow for troubleshooting comment 2022-05-29 13:10:21 -04:00
Mike Verbanic
773e856e31
update changelog (#188) 2022-05-27 14:25:14 -04:00
Mike Verbanic
d4641feafe
chore: update release process (#187)
* update release process

* Apply suggestions from code review

Co-authored-by: Seth Vargo <seth@sethvargo.com>

Co-authored-by: Seth Vargo <seth@sethvargo.com>
2022-05-27 10:44:10 -04:00
Google GitHub Actions Bot
ceee102ec2
chore: release 0.8.0 (#186) 2022-05-23 15:47:48 -04:00
Mike Verbanic
b60aafc09d
chore: release 0.8.0 (#185)
Release-As: 0.8.0
2022-05-23 15:43:57 -04:00
Google GitHub Actions Bot
9025e64312
Build dist (#184) 2022-05-23 15:28:42 -04:00
Google GitHub Actions Bot
30b1c78d51
chore: release 0.8.0 (#183) 2022-05-23 15:27:20 -04:00
Mike Verbanic
95a6bc2a27
feat: add retries (#181) 2022-05-23 15:17:21 -04:00
Google GitHub Actions Bot
10d8e00a99
Build dist (#180) 2022-05-18 14:10:26 +00:00
Google GitHub Actions Bot
81012c2689
chore: release 0.7.3 (#179) 2022-05-18 10:09:11 -04:00
Seth Vargo
7c32666372
fix: repair a logic bug for domain-wide delegation (#178) 2022-05-18 10:04:31 -04:00
Google GitHub Actions Bot
714f1fe243
Build dist (#177) 2022-05-16 10:07:45 -04:00
Google GitHub Actions Bot
dafc92490a
chore: release 0.7.2 (#175) 2022-05-16 10:06:39 -04:00
Seth Vargo
cf96743e5a
chore: document clock skew issue 2022-05-16 09:59:39 -04:00
Seth Vargo
a1f1aca0d6
chore: build 2022-05-16 09:50:59 -04:00
Seth Vargo
f322a0d1e2
fix: only emit warning if the lifetime exceeds 1 hour 2022-05-16 09:48:11 -04:00
Seth Vargo
ce2cf1ac23
chore: update deps (#173) 2022-05-02 10:38:47 -04:00
Google GitHub Actions Bot
b258a9f230
chore: release 0.7.1 (#169)
Co-authored-by: Seth Vargo <seth@sethvargo.com>
2022-04-22 16:48:59 -04:00
Google GitHub Actions Bot
e2458b7f8a
Build dist (#168) 2022-04-22 16:48:29 -04:00
Seth Vargo
b5b6d978c9
chore: release 0.7.1
Release-As: 0.7.1
2022-04-22 16:47:49 -04:00
Seth Vargo
c2d3c1fca0
chore: version bump 2022-04-22 16:47:11 -04:00
Seth Vargo
f6793d5041
chore: update all deps (#167) 2022-04-22 16:46:20 -04:00
Seth Vargo
9adccd9156
docs: add link to gsutil issue about WIF support 2022-04-19 10:58:13 -04:00
Google GitHub Actions Bot
b05f71482f
Build dist (#163) 2022-04-05 23:46:05 +00:00
Google GitHub Actions Bot
50dbfd0907
chore: release 0.7.0 (#158) 2022-04-05 15:07:42 +00:00
Seth Vargo
e426d954f6
chore: update references to setup-gcloud@master -> setup-gcloud@main (#162) 2022-04-05 09:44:37 -05:00
Seth Vargo
d16fd896f7
feat: allow opt-out of exporting environment variables (#157)
This allows users to opt-out of exporting environment variables. This might be helpful if they **don't** want future steps to be authenticated, or if the exported environment variables conflict with other values.
2022-03-24 16:02:18 -05:00
Seth Vargo
38d3c2f54d
chore: update deps (#155) 2022-03-21 12:52:27 -04:00
Seth Vargo
e9ca24f3e2
docs: add a node about node16 on self-hosted runners (#149) 2022-03-08 10:24:24 -05:00
Koichi Shiraishi
db6919d074
Update actions/checkout to v3 (#148)
* chore: update actions/checkout to v3

Signed-off-by: Koichi Shiraishi <zchee.io@gmail.com>

* chore: update docs to actions/checkout@v3

Signed-off-by: Koichi Shiraishi <zchee.io@gmail.com>
2022-03-08 10:18:30 -05:00
Averi Kitsch
8d936a3c13
chore: add dependabot settings (#146) 2022-03-02 08:44:27 -08:00
SHIMADA Kento
65963dbda5
doc: fix registry name example for GAR (#143) 2022-02-25 12:56:50 -05:00
Charles Sullivan
3fbe03175e
doc: Fix token_format in readme. (#138)
https://github.com/google-github-actions/auth/blob/main/action.yml#L136-L139
2022-02-14 11:37:07 -06:00
Google GitHub Actions Bot
a2b753f630
Build dist (#133) 2022-02-03 15:58:48 -05:00
Google GitHub Actions Bot
8d125895b9
chore: release 0.6.0 (#118) 2022-02-03 15:55:30 -05:00
Google GitHub Actions Bot
696f31dbf6
Build dist (#131) 2022-02-03 15:53:26 -05:00
Seth Vargo
f9dc3d62d1
doc: add more troubleshooting (#132) 2022-02-03 14:25:36 -06:00
Seth Vargo
48c46e6a59
feat: ensure cred file is created with a predictable name (#130) 2022-02-03 11:57:50 -06:00
Seth Vargo
3b7fb59565
docs: add gar and gcr example (#128) 2022-02-03 14:30:22 +00:00
Seth Vargo
b6d69ec4d4
doc: add troubleshooting section (#127) 2022-02-03 02:19:28 -06:00
Seth Vargo
5d9db97c33
chore: minify build again 2022-01-30 17:08:54 -05:00
Seth Vargo
736e11de40
chore: update deps and use isEmptyDir from utils (#119) 2022-01-26 17:47:12 -06:00
Seth Vargo
983a037dfb
feat: emit a warning when the workspace is empty (#117)
There have been a number of GitHub issues recently due to users not adding actions/checkout before calling "auth", which makes the credentials unavailable to future steps. Worse, some people are putting checkout _after_ auth, which overwrites the generated credentials with a checkout of the repo.

This adds a feature that emits a warning with the workspace is empty.
2022-01-26 15:27:51 -05:00
Google GitHub Actions Bot
ac489d50bb
Build dist (#114) 2022-01-22 19:54:07 +00:00
Google GitHub Actions Bot
7a360a247c
chore: release 0.5.0 (#112) 2022-01-22 14:52:49 -05:00
Google GitHub Actions Bot
7b354992e5
Build dist (#113) 2022-01-22 14:46:54 -05:00
Seth Vargo
88fbfac1f6
docs: note checkout must come first 2022-01-22 14:41:04 -05:00
Seth Vargo
54924dbbed
feat: switch to use node version 16 (#110) 2022-01-22 14:38:43 -05:00
Tautvydas Versockas
1261433f2b
Replace single quotes with double quotes in README (#108) 2022-01-17 16:25:00 +00:00
Seth Vargo
2c3f0ad713
Add note about gcloud version 2022-01-11 11:41:30 -05:00
Seth Vargo
f616570cea
Fix small README typo 2022-01-07 09:46:48 -05:00
Seth Vargo
9b6531516d
bug: use actions-utils to emit HEAD warning (#102) 2021-12-30 15:00:21 +00:00
Seth Vargo
478002e00b
chore: fix readme example to not consume token (#98) 2021-12-29 22:01:45 -06:00
Seth Vargo
419a2c3bfe
chore: warn if the action is pinned to HEAD (#99) 2021-12-29 12:58:41 -05:00
Seth Vargo
443ae925ab
bug: only cleanup credentials if credentials were created (#96) 2021-12-23 11:39:54 -05:00
Google GitHub Actions Bot
8c15757ad6
Build dist (#95) 2021-12-22 16:51:21 +00:00
Google GitHub Actions Bot
c6c22902f6
chore: release 0.4.4 (#93) 2021-12-22 11:48:33 -05:00
Seth Vargo
0aa6edff98
Only run integration when secrets are available 2021-12-22 11:31:56 -05:00
Seth Vargo
c2df8017cd
Drop install_and_compile requirement 2021-12-22 11:19:39 -05:00
Seth Vargo
b39ebee9c1
Temporarily unminify 2021-12-22 11:12:54 -05:00
Seth Vargo
ec59fe8c8e
fix: switch to actions-utils and update deps (#91) 2021-12-22 10:45:18 -05:00
Seth Vargo
5975d7549d
Update release process (#92) 2021-12-22 10:34:57 -05:00
Seth Vargo
1b8ec4e237
Update README.md 2021-12-19 22:13:23 -05:00
Seth Vargo
096ab1fb9a
Revert "bug: trigger tag workflow on lightweight tag creation too"
This reverts commit 1be7cd0ce2.
2021-12-15 16:59:00 -05:00
Seth Vargo
1be7cd0ce2
bug: trigger tag workflow on lightweight tag creation too 2021-12-15 16:40:37 -05:00
Katie McLaughlin
52d3869752
(minor) fix syntax on README step (#88) 2021-12-14 12:08:46 -05:00
Bharath KKB
1c6c798434
chore: add a link to TF module (#86) 2021-12-09 17:23:08 -06:00
Seth Vargo
ac09c292a1
Update documentation and emit log messages (#85) 2021-12-09 17:27:21 -05:00
Seth Vargo
d03480e8ad
Update documentation 2021-12-09 14:15:09 -05:00
Seth Vargo
5090ecb28d
Add workflow to update tag pointer (#82) 2021-12-09 14:14:46 -05:00
Seth Vargo
ccc7806970
Emit a better error when OIDC information is missing (#81) 2021-12-09 12:52:57 -05:00
Seth Vargo
1618f1c032
Add another test for fromBase64 (#76) 2021-12-07 10:22:31 -06:00
Seth Vargo
870ff908cc
Allow manually invoking tests too (#74) 2021-12-06 17:19:37 -06:00
Seth Vargo
ed829381c8
Upgrade deps (#72) 2021-12-03 14:00:48 -06:00
Seth Vargo
abba7c4d9a
Remove bug and pr templates to inherit from organization (#71) 2021-12-02 17:57:29 -05:00
Seth Vargo
8708e498da
Add support for Domain-Wide Delegation (#70) 2021-12-02 10:17:06 -06:00
Seth Vargo
057960bb62
Add util function for parsing durations and many more tests (#69) 2021-12-01 16:13:51 -05:00
Seth Vargo
1e9245c68a
Clean up exported credentials when the workflow finishes (#67)
* Clean up exported credentials when the workflow finishes

* Fix conditional and log
2021-12-01 11:38:47 -06:00
Seth Vargo
c6fa692def
Use strict everywhere (#68) 2021-11-30 13:23:28 -08:00
Seth Vargo
a37d7b422e
Update README.md (#64) 2021-11-29 18:10:56 -06:00
Nozomu Ohki
6dbf4e537f
chore: Replace backticks with single quotes in README (#65) 2021-11-29 17:52:40 -06:00
Seth Vargo
e805d1d054
Update README to v0.4.1 2021-11-29 14:57:02 -05:00
Bharath KKB
9a051ab76c
fix: export Google GHA creds env var (#57)
* fix: export Google GHA creds env var

* add comments for cred envvars

* build
2021-11-26 11:02:14 -05:00
Seth Vargo
2428105372
Compile 2021-11-25 14:28:51 -05:00
Seth Vargo
ddbc4798b7
Make error message clearer that it came from auth 2021-11-25 14:26:11 -05:00
Seth Vargo
fc5003a59a
Update README.md 2021-11-25 14:20:31 -05:00
pokutuna
e7aec2b7c8
docs: fix token format example in readme (#59) 2021-11-25 14:19:48 -05:00
Seth Vargo
cc4b368646
Output more information in error about missing secrets (#55) 2021-11-22 11:51:55 -05:00
Seth Vargo
c022bd29f8
Add log output to bug template (#53) 2021-11-22 11:27:32 -05:00
Seth Vargo
3ecfcdb218
Fix issue template 2021-11-11 14:15:40 -05:00
Seth Vargo
6c3096cce2
Use YAML templates for issues (#47) 2021-11-11 14:14:37 -05:00
Seth Vargo
f56f5989ce
Pull user-agent version from module (#45) 2021-11-10 13:39:13 -06:00
Bharath KKB
19e4d7845e
chore: switch to using test-infra resources (#44)
* chore: switch to using test-infra resources

* swap secret name
2021-11-09 21:40:23 -06:00
Seth Vargo
a61909d048
Description < 125 characters 2021-11-09 16:13:20 -05:00
Seth Vargo
b759bed608
Bump README to v0.4.0 2021-11-09 16:11:11 -05:00
Seth Vargo
f014f2369a
Fallback to ref (#43) 2021-11-09 15:08:31 -06:00
Seth Vargo
d5ed5bab89
Handle non-pull request runs (#42)
github.head_ref is only available in PRs, so the build failed on main
2021-11-09 11:59:19 -06:00
Seth Vargo
d97e31546b
Ensure files are compiled as part of CI (#41)
* Ensure files are compiled as part of CI

* Install deps there too
2021-11-08 20:01:39 -06:00
Seth Vargo
2f0b4dbd9b
Add support for specifying authentication via JSON service account keys (#37)
* Add support for specifying authentication via JSON service account keys

* Update README.md

Co-authored-by: Bharath KKB <bharathkrishnakb@gmail.com>

* Update README.md

Co-authored-by: Bharath KKB <bharathkrishnakb@gmail.com>

* Update README.md

Co-authored-by: Bharath KKB <bharathkrishnakb@gmail.com>

* Review feedback

* Undo interface

* Use TokenCreator instead

Co-authored-by: Bharath KKB <bharathkrishnakb@gmail.com>
2021-11-08 17:13:59 -05:00
Bharath KKB
d5a354ef10
chore: refactor WIF (#33)
* define common interfaces

* common base client

* refactor WIF to use interfaces and base client

* refactor main

* add build in CI

* add name for build step

* address comments

* fix import

* interface for credfile return

* regen dist
2021-10-12 23:17:42 -04:00
Seth Vargo
3fe2a3779a
Expand error message when GitHub envvars are not present (#31)
This points people to the GitHub Actions permissions documentation, which will help with troubleshooting token permission errors.
2021-10-06 11:26:23 -07:00
Seth Vargo
90e80b8d07
Update to new STS URL (#30)
Per https://twitter.com/chrisrpatterson/status/1445773065221128203. This is still rolling out.
2021-10-06 11:22:19 -07:00
Seth Vargo
abe0e87145
Bump version used in README examples (#26) 2021-10-04 09:35:28 -07:00
Seth Vargo
f3c3e206c9
Default audience to the WIF provider ID (#23) 2021-10-04 09:14:08 -07:00
Seth Vargo
02f3d58995
Clean up a few typescript warnings (#25) 2021-09-30 13:07:11 -07:00
Seth Vargo
60904d8a0d
Upgrade actions/core to release version (#21) 2021-09-28 17:07:49 -04:00
Seth Vargo
b3b82c7f1e
chore: Add issue/pr templates, note test failures likely (#19) 2021-09-27 19:33:43 -05:00
Bogdan A
7296e5030f
docs: remove unnecessary steps and update examples (#15) 2021-09-27 12:59:53 -04:00
Niels Hofmans
f8bb88e07e
chore: add read privileges to readme (#18) 2021-09-27 11:21:07 -04:00
Yuki Furuyama
0583f8fbeb
s/access_token/id_token/ for id_token_include_email input (#13) 2021-09-23 20:45:22 -04:00
Seth Vargo
fe9d1eddf5
Update README.md (#14) 2021-09-23 20:44:24 -04:00
Seth Vargo
1854238d2d
Add a note about overriding default permissions
Fixes GH-12
2021-09-22 21:31:59 -04:00
Seth Vargo
5f24906e9b
Create CODEOWNERS (#11) 2021-09-22 10:10:39 -04:00
gregoireW
f660f85abd
Fix a typo in the README for usage instructions
Signed-off-by: gregoireW <24318548+GregoireW@users.noreply.github.com>
2021-09-22 08:59:42 -04:00
Seth Vargo
fb26879445
Update references to google now (#8) 2021-09-21 19:30:30 -04:00
Seth Vargo
2dd133ffa2
Bump version 2021-09-21 18:11:17 -04:00
Seth Vargo
febe21311b
Add the ability to generate and export a credentials file (#7)
This credentials file can be passed to gcloud or other Google Cloud SDKs to automatically do the exchange.
2021-09-21 18:10:27 -04:00
Seth Vargo
c7bb6ad28f
access_token_scopes is not required 2021-09-21 12:25:09 -04:00
Seth Vargo
bbbd1424f7
Add example of attribute mapping repos 2021-09-21 12:23:39 -04:00
Seth Vargo
03500c2f23
Fix readme 2021-09-20 21:29:09 -04:00
Seth Vargo
ddb73aed67
Set user-agent header 2021-09-20 21:02:56 -04:00
Seth Vargo
ad2dddb569
Even more README fixes 2021-09-20 11:44:31 -04:00
Seth Vargo
5e33466111
Fix readme 2021-09-20 11:41:07 -04:00
Seth Vargo
cb396c3f31
Refactor to support access and id tokens (#3) 2021-09-18 12:12:21 -04:00
Bharath KKB
afef6a5b6d
feat: use actions/core for getting gh OIDC token (#2)
Co-authored-by: Seth Vargo <seth@sethvargo.com>
2021-09-18 11:35:52 -04:00
Yuki Furuyama
e13dfdd573
Support ID Token generation (#1)
* Support id token generation

* Fix id_token_audience validation

* Add id_token_audience to test workflow

* Generate dist/index.js for id token support
2021-09-18 11:34:46 -04:00
Seth Vargo
271e0346a0
Add example 2021-09-16 13:00:02 -04:00
41 changed files with 4976 additions and 4845 deletions

View file

@ -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
View 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
View 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
View 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
View 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
View 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 }}'

View file

@ -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
View 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
View 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
View file

@ -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

View file

@ -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
View 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
View file

@ -0,0 +1 @@
* @google-github-actions/maintainers

691
README.md
View file

@ -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.
[![Authenticate to Google Cloud from GitHub Actions with Direct Workload Identity Federation](docs/google-github-actions-auth-direct-workload-identity-federation.svg)](docs/google-github-actions-auth-direct-workload-identity-federation.svg)
> [!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.
[![Authenticate to Google Cloud from GitHub Actions with Workload Identity Federation through a Service Account](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)
<details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Workload Identity Federation through a Service Account.</summary>
These instructions use the [gcloud][gcloud] command-line tool.
1. (Optional) Create a Google Cloud Service Account. If you already have a
Service Account, take note of the email address and skip this step.
```sh
# 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.
[![Authenticate to Google Cloud from GitHub Actions with a Service Account Key](docs/google-github-actions-auth-service-account-key-export.svg)](docs/google-github-actions-auth-service-account-key-export.svg)
> [!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

View file

@ -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
View file

@ -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

File diff suppressed because one or more lines are too long

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
View 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

View 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
View 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

View file

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

After

Width:  |  Height:  |  Size: 36 KiB

View file

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

After

Width:  |  Height:  |  Size: 46 KiB

18
eslint.config.mjs Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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
View 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';

View 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}`,
);
}
}
}

View 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));
}
}

View 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
View 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);
}
}

View file

@ -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
View 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
View 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';
}

View file

@ -1,6 +0,0 @@
import { expect } from 'chai';
import 'mocha';
describe('Client', () => {
it('todo');
});

View 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);
});
});

View 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
View 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/);
}
});
});

View file

@ -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/"]
}