mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 14:21:39 +00:00
Compare commits
384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa52d9e89e | ||
|
|
6383aa594a | ||
|
|
54eb59c27b | ||
|
|
3877f8309b | ||
|
|
d8b0681831 | ||
|
|
cd8303dbea | ||
|
|
ab51d6e931 | ||
|
|
b7e402dca2 | ||
|
|
842040a8b3 | ||
|
|
1205da5d34 | ||
|
|
f40824fedd | ||
|
|
67fa0cbc61 | ||
|
|
e029c77f76 | ||
|
|
2b23ae4e67 | ||
|
|
c8ecc23c9c | ||
|
|
94f8959879 | ||
|
|
2cdb8eb44d | ||
|
|
ebc1d1ecb3 | ||
|
|
d9e99334d2 | ||
|
|
a06776bbbd | ||
|
|
aecd725a71 | ||
|
|
ad8e9364e1 | ||
|
|
b812fef1c3 | ||
|
|
56371214b0 | ||
|
|
3669a86f41 | ||
|
|
4095bf63ef | ||
|
|
d75321ca8a | ||
|
|
c931e1cdd7 | ||
|
|
ea8cda72c7 | ||
|
|
f0bcd7888a | ||
|
|
6a08a66221 | ||
|
|
5e58edf598 | ||
|
|
1e79772ec1 | ||
|
|
3461ce053f | ||
|
|
9c84ce30e8 | ||
|
|
93ca63e133 | ||
|
|
e367c86fce | ||
|
|
34dc12994a | ||
|
|
df4de5ce4b | ||
|
|
ea630480b2 | ||
|
|
db1159cd0d | ||
|
|
ccf1dc0585 | ||
|
|
1cb96cf057 | ||
|
|
2bc8a114c1 | ||
|
|
7f183b9edc | ||
|
|
f86be17834 | ||
|
|
6854e3729a | ||
|
|
5f48b1e16f | ||
|
|
aea92be8d3 | ||
|
|
b4c5d6c626 | ||
|
|
0e2845082b | ||
|
|
705a40d035 | ||
|
|
f85a072708 | ||
|
|
e990387660 | ||
|
|
feb76f504c | ||
|
|
6a33173a49 | ||
|
|
5e8e9765fa | ||
|
|
2a6cdb9b14 | ||
|
|
642c0fa216 | ||
|
|
9d5e79725c | ||
|
|
db301d2635 | ||
|
|
d7283d17e2 | ||
|
|
bd7b58ad43 | ||
|
|
27c0e5f8a6 | ||
|
|
7a439a3e07 | ||
|
|
fff6e6717f | ||
|
|
2671764e99 | ||
|
|
4f12e2affb | ||
|
|
e1faab524b | ||
|
|
6c5585d059 | ||
|
|
dc678dd510 | ||
|
|
7b70c5a745 | ||
|
|
2e8190ce29 | ||
|
|
14b09b91df | ||
|
|
b77d7eaadc | ||
|
|
c68e0bfbc4 | ||
|
|
8a26e99dfc | ||
|
|
9e2dd11617 | ||
|
|
19f01007f4 | ||
|
|
2c4bbbbbfb | ||
|
|
71536c50a2 | ||
|
|
1d118a9ca3 | ||
|
|
94f6c45291 | ||
|
|
fd4a64f6a7 | ||
|
|
d8ba15ab98 | ||
|
|
a0a2e1359e | ||
|
|
34b32da1e6 | ||
|
|
f9af688bea | ||
|
|
8829bc3a65 | ||
|
|
59d8e5a853 | ||
|
|
fddd77e87c | ||
|
|
eed3fb1ed9 | ||
|
|
fbaf9272a8 | ||
|
|
97df7c75b1 | ||
|
|
358892869b | ||
|
|
7cca371c22 | ||
|
|
8b1c8b36ce | ||
|
|
86154adc92 | ||
|
|
bfc0b57f62 | ||
|
|
d67110e771 | ||
|
|
2d7c382c2f | ||
|
|
4536cd13ef | ||
|
|
627db97b30 | ||
|
|
bbdce9536b | ||
|
|
5f0644d924 | ||
|
|
0b5dffc5a5 | ||
|
|
33c85e9ed6 | ||
|
|
c04164e47a | ||
|
|
06e7545f33 | ||
|
|
0399d96fd4 | ||
|
|
347688ac8c | ||
|
|
37ce64acb7 | ||
|
|
4b8bc4e7ea | ||
|
|
d9ba83217f | ||
|
|
1d658ca1ac | ||
|
|
65f94ff465 | ||
|
|
1b010fbd07 | ||
|
|
a1bce42387 | ||
|
|
4808f18ca0 | ||
|
|
b41baf19b4 | ||
|
|
9d78fa8825 | ||
|
|
2a6a424ce0 | ||
|
|
c05c040241 | ||
|
|
3d3f2b535b | ||
|
|
49bf1f675a | ||
|
|
50f3ac493c | ||
|
|
690302f2b3 | ||
|
|
af7b387f94 | ||
|
|
1c38c47e4a | ||
|
|
74525efc37 | ||
|
|
985db74216 | ||
|
|
6d4a34d971 | ||
|
|
43b17cce0e | ||
|
|
0fc11de8bf | ||
|
|
39f5b1dbc2 | ||
|
|
f3596856ba | ||
|
|
fc7e8b4deb | ||
|
|
4178b4a61e | ||
|
|
05d5fdde53 | ||
|
|
3be1e97863 | ||
|
|
0cffb8202d | ||
|
|
657ce1cbbf | ||
|
|
f8ee80be19 | ||
|
|
70de8b8719 | ||
|
|
f972f0b8e4 | ||
|
|
8c025758c9 | ||
|
|
2713bd9bfc | ||
|
|
cdc067c4b7 | ||
|
|
a18ab16d9d | ||
|
|
7bdde3ec68 | ||
|
|
2c89f988a1 | ||
|
|
ead7c7403a | ||
|
|
5ce67e5f5c | ||
|
|
b34b2d8e2a | ||
|
|
64d1dec3d8 | ||
|
|
7f14bbcc22 | ||
|
|
9396cdef4a | ||
|
|
b49f759ef9 | ||
|
|
3087e0cbdb | ||
|
|
9992eaad2b | ||
|
|
33f5e74e45 | ||
|
|
9a068f02da | ||
|
|
b71a4f1d0b | ||
|
|
42950550cc | ||
|
|
86842bbb02 | ||
|
|
4ca0277e63 | ||
|
|
671ac52201 | ||
|
|
78c71babda | ||
|
|
19196a2ee4 | ||
|
|
843c5f0687 | ||
|
|
260160ade1 | ||
|
|
06525ff690 | ||
|
|
ac2ccc2d03 | ||
|
|
41eba0c873 | ||
|
|
f6672496d1 | ||
|
|
004b51f3cd | ||
|
|
1046716304 | ||
|
|
187b7a8c39 | ||
|
|
27177e3ef4 | ||
|
|
367eaae47f | ||
|
|
dcee433547 | ||
|
|
ea74e24024 | ||
|
|
6353ddf58a | ||
|
|
911d83cfd8 | ||
|
|
49fc1c4f7e | ||
|
|
097eb07fcc | ||
|
|
00f992259b | ||
|
|
ce655173f8 | ||
|
|
ec1c146362 | ||
|
|
4098a9b70f | ||
|
|
b617d5cab0 | ||
|
|
4b29fda924 | ||
|
|
37d07c9bd8 | ||
|
|
0cf964073d | ||
|
|
298e161658 | ||
|
|
997b2e84da | ||
|
|
c135866f0d | ||
|
|
509f125e3a | ||
|
|
90c51a2d51 | ||
|
|
2b58539588 | ||
|
|
8b3bf26edf | ||
|
|
d4190496fb | ||
|
|
a8ceb40953 | ||
|
|
e47b6a32de | ||
|
|
77ed31d975 | ||
|
|
2d4a24554f | ||
|
|
4fcade37c2 | ||
|
|
71978841b1 | ||
|
|
6aab88abce | ||
|
|
7bc5ad35cb | ||
|
|
751020a589 | ||
|
|
fdfd6e80fb | ||
|
|
ee3b6e471b | ||
|
|
b818d6a1c1 | ||
|
|
221cd9826d | ||
|
|
5afda1dc2f | ||
|
|
108ca38f10 | ||
|
|
f104c7acdc | ||
|
|
a62a7d4da4 | ||
|
|
2f0fa38f3d | ||
|
|
2243a4e8eb | ||
|
|
4e15cb6618 | ||
|
|
2e103a2d69 | ||
|
|
22c7609e0d | ||
|
|
04504cdb2e | ||
|
|
294211bf84 | ||
|
|
0f7e0a4c87 | ||
|
|
351e9ab929 | ||
|
|
6d16eae210 | ||
|
|
f0d0345fcd | ||
|
|
1c8cb1a617 | ||
|
|
bc065d4b31 | ||
|
|
71c9d90b39 | ||
|
|
dd7d4923be | ||
|
|
f348405d46 | ||
|
|
7e37e7298e | ||
|
|
ed56599260 | ||
|
|
457ce15a4c | ||
|
|
892d200cd2 | ||
|
|
0b763b70f4 | ||
|
|
9310771832 | ||
|
|
a590c0487e | ||
|
|
46fcd84ecb | ||
|
|
f8f492efb2 | ||
|
|
ebe29d3a5a | ||
|
|
92b8ea6dcd | ||
|
|
1654dee62e | ||
|
|
2316b9e76a | ||
|
|
b095b361cb | ||
|
|
fe7c72beac | ||
|
|
9930209980 | ||
|
|
48553f4ad9 | ||
|
|
a7bb600bf5 | ||
|
|
57938b19a8 | ||
|
|
8d485f4f74 | ||
|
|
8db6d1f4f7 | ||
|
|
6198f7ace3 | ||
|
|
9472fb2a4c | ||
|
|
8fdc49a0af | ||
|
|
639cede6c4 | ||
|
|
65fd49e377 | ||
|
|
4e141bf764 | ||
|
|
2db40e841b | ||
|
|
1f728ff4da | ||
|
|
4c43b28820 | ||
|
|
e6610357e3 | ||
|
|
8ad2f8d633 | ||
|
|
127d354972 | ||
|
|
a099981b3c | ||
|
|
857230def3 | ||
|
|
693c23e562 | ||
|
|
5c6e31200e | ||
|
|
074318d797 | ||
|
|
838b377d4b | ||
|
|
f1061d7190 | ||
|
|
948e40f51a | ||
|
|
fb6746e694 | ||
|
|
e68632e840 | ||
|
|
049319c499 | ||
|
|
958c2ee356 | ||
|
|
6b569df751 | ||
|
|
7587a58dc3 | ||
|
|
7ea3930975 | ||
|
|
cccaf5fe54 | ||
|
|
932dbd74ee | ||
|
|
4cbb2300df | ||
|
|
ecb4efa352 | ||
|
|
03d7fe0407 | ||
|
|
3f6dff1543 | ||
|
|
9857869799 | ||
|
|
8879c5d8c7 | ||
|
|
7d1edd41c7 | ||
|
|
970542eb7f | ||
|
|
5270929b57 | ||
|
|
6543ead625 | ||
|
|
817d618d08 | ||
|
|
a0833393a8 | ||
|
|
811a2db58b | ||
|
|
8b9af97012 | ||
|
|
36b874d8a0 | ||
|
|
5680bc05b5 | ||
|
|
90b8751874 | ||
|
|
51712af78d | ||
|
|
f5681cb746 | ||
|
|
85d52a4d68 | ||
|
|
cec7f88d2b | ||
|
|
4043bcab61 | ||
|
|
1efa50aeab | ||
|
|
400fa08ebc | ||
|
|
eda1a1de24 | ||
|
|
31d8cd09dd | ||
|
|
91b3ddb489 | ||
|
|
2392d575c2 | ||
|
|
8b47748df9 | ||
|
|
7a9899f747 | ||
|
|
2e5e784f4c | ||
|
|
5b9d14e966 | ||
|
|
5fb0b1b5ff | ||
|
|
a08d95a742 | ||
|
|
64380cbd4d | ||
|
|
837d98deaf | ||
|
|
a63ac294db | ||
|
|
d20a0d8455 | ||
|
|
cd710e99c0 | ||
|
|
1cedbc9423 | ||
|
|
2ce5915e70 | ||
|
|
e602d3fef0 | ||
|
|
c6e1e5c1cf | ||
|
|
837bff58e7 | ||
|
|
01d50adce7 | ||
|
|
05cbe1c6f3 | ||
|
|
939bd8d9ab | ||
|
|
204a689848 | ||
|
|
a852c7e5ca | ||
|
|
b3ad45f2cc | ||
|
|
9f988bb464 | ||
|
|
ce0ad0a3ea | ||
|
|
c5daf892c6 | ||
|
|
bd484cbe41 | ||
|
|
cbfb0a7310 | ||
|
|
45027da057 | ||
|
|
fb53f2ed0e | ||
|
|
37e0e8942e | ||
|
|
ef86508bbb | ||
|
|
f6459e20f9 | ||
|
|
aeff5edaeb | ||
|
|
d274d05336 | ||
|
|
207eb0990c | ||
|
|
1cbe8297aa | ||
|
|
5b8fcebabd | ||
|
|
820f17ce74 | ||
|
|
753ecd7244 | ||
|
|
df3ea385ee | ||
|
|
eb041e9e65 | ||
|
|
96eb2496e4 | ||
|
|
cbe89c8c40 | ||
|
|
4d893c4da1 | ||
|
|
9dd0b135b9 | ||
|
|
5b0439ddb5 | ||
|
|
c3cb82a2de | ||
|
|
516f13bf48 | ||
|
|
c3bf18cf5a | ||
|
|
ece2d2943e | ||
|
|
610dbd4dcf | ||
|
|
fe3c043d61 | ||
|
|
83d8135722 | ||
|
|
6caf3f2252 | ||
|
|
b8331a3a4a | ||
|
|
c271349c30 | ||
|
|
329a6e0768 | ||
|
|
fae2dca9dc | ||
|
|
7112a42c96 | ||
|
|
d76618cbad | ||
|
|
d8db37786c | ||
|
|
79bc1065f3 | ||
|
|
b107afc13c | ||
|
|
fcfbf0a733 | ||
|
|
8460a7a87d | ||
|
|
2940dd71ab | ||
|
|
135ebaa251 | ||
|
|
f94b3eb383 | ||
|
|
a3db496f31 | ||
|
|
dd78c05d59 | ||
|
|
d9c4326a6b |
237
.travis.yml
Normal file
237
.travis.yml
Normal file
@@ -0,0 +1,237 @@
|
||||
if: tag IS blank
|
||||
|
||||
env:
|
||||
global:
|
||||
- BUILD_PYTHON_VERSION=3.7.4
|
||||
- BUILD_OPENSSL_VERSION=1.1.1c
|
||||
- PATCHELF_VERSION=0.9
|
||||
- MUSL_VERSION=1.1.22
|
||||
- secure: "FSKvLaiqhKz21SVgAQZI3bSX34Ffyev4l+R2G//QXNDu6UVQcuFsykzw+eZEG7fkhotXr8BMDL7xIkookiL8eLwUtcd/Z95HCjPBBHcmCSQleyvuuJBxdrQ9xldmiGLzMCYiumSH9OH4uJhQ39Yjnjsa8TK+PlTci6a/BTzlYyBSyDYDf7Iv/uhfQPDHL3pNwrQPHf4fL6/jcvo+uaPcv83AVZkNzZjjyoi9Aa+uh9xlbyHg11jp44463qqxoxTdYik3pYuXRBPjknjOGcnFHqn+QOVSdRQoiwbmT8xVuYuCzTv9THhuJ//i5u7s4y3Xyl7u17B3tdm86UlMpQHy/w9EsYaSBPOU4oPNomRtOnTSugh0v9ZBwptP5XfbslII/iA+LQdzTHhchn0W0CRyDqjOMSestWlrsq5NZJtBJTYHbebllOhEI7xbj9tY+re1zFWSPMOPgHJP23ovsdk3hD9OT93AzRHInCx5IxL6QvEgRhAancRuGkf2rGP0g/vX9fQ0Il3rNMSQxHB5CyHUBtUJ9nhU79YkMDZicD0jFMEwjWJO3itAp3ynoLXRgktgQCYUfgc9SpdWKD5SXLCYnSo22JD3D1P6h2EertRHaoKRLb+CRXQC/lM8uh/W+BjA2Xe6Vut2I/72ndjM+10T7E2xk1CFyCH37a5p8cH26Fs="
|
||||
- secure: "J9380tGLOZWa7dSH1y5Il8T5JQpN6ad81gI6VR1HIU0svpRdjgikyDA7ca2MKYDUYYY9yVSkTV6gCl6iIU/9+SKaYugpP+tkvdGYkC2moJdcTgYM/WOnIK9ExQ3BPhN1neGxJjPTwKo1ft27mtZ2I5vuCiBwIcnKWLnKPyW3PD+mWpfqiLuEzkHoAh6G3jC4qbcCrZDeX/knE+PzqESUEi+8k1G8gYcSDWujba9ypSsqZ8T/MXagGla6l7y2Rz+/KZTJmFHwKAA10V+xPLVqxoiqi4ar66yUqy0BamwRXPcseI+ns3Q+4lUpMqVQ5GlRy7LF1xC8myjmcAexXk0F9hg+CMzewKI8UgmQH/ZJvQZEh8s6mW26+CqA4d3zMQkWaR0WtEtpiuH7AGHCflIqvEQ6UiG7ia3B8iZfW2wl0j/kqx4OuHkS3r0pWKVVIIvCj9Ow2BHP7SpiV1AcUGsVxzwbgTh67fitna3Z3c6Uj8ccQlNr7ZIt1az6Wf3w5njijkLOiBpQSLKunTTCTSge/JzBTKUcie3RE9vzirl58gUxAt36nDtPWnory+RttMZrOkBVbTeSxp+IUe8pNwLFPHABsafXsjkfzBOtFmm+0ZXWt2Rlog5NvlemJfQUWDlsL4g+BSakzN+4sIPKzSauWDHyaEeULY7Uprkil6c5zwo="
|
||||
- secure: "szcjWHPr0Bf1KCkyTrV5Fu3ADhWk+pg8YWucjXHdybmhaQIKG7iBNg8LJ5d0OBTwAg31wK4ZgyLVSa2gKrAZ3UeDjykJFsR711xDSQOod51Wrgqu4FbXDewE817DUk3Cwe1l5DCu3/fjEw4vbm8B/qb7iMTRKCq6hJd97FwT5oauP0QHNPer9JjrW4F0Hk9ttkgEU2dXWvBMsTJsDOGNI3ddABE2HskxV4T4thelDYGKBDHhUOAsRwSjXgWy77Tvz98psPIvd+6+WPYNRdRWcPDyAR3Z1O/fNjUymrQI6eMaHoSFrmhDS5lbhjINRfdUmECyfCfIFeLWWiw4g4bq7l+4HBORbei55tAIjhEsxJQoqHi0Q5dD5TFh8IiWqowkFbpvNonMSIpKtB0cyT5jU1G/jRA7MPcIvSrdzHaDkoDNHJgAeZfgjOhzTGYYD19lGIljz5BQBcNFZY2dJbja+Jr4He2CMAOBOdERa4Zn1VyNfOmd8Bn5hu0C9D2ybnSCxjXXq5TRiktR8X7WycVZYfqMZXAwP9FEHVitJ4MZEGUc7S92K5gX4wmjcJjLS+Xo/0nsduQm8PuiMjbcPM7/oGx8Xm1KuSfHdKWMBoaesPaDvRX+YcuiNstXf1DkCWl72TsFABzddlNUMl/s2YSKkCSHAJ5ILqrB28Gx89kzVlg="
|
||||
- secure: "CPsDgSoZIHLyjpUbYEsx1GbB+UZcPXCEsz9qT6XRM+qMFKLlSnHxoJ7gMrJtqfjTh+1gLB3UTjQjFr+jAenqLWzaJh7Zdtpg9BOG6aXC8CAi1h0U5ZMNSA/+5lQxOXuhN8HmXI1r8xPNRFXBdZFea+072/2HJTwmgYlVTTQ8FrbXQJCzs2cFqxnVeFmuG3N49AJuoy3B+P2DMpqPzABbQt7Jf5H9Foq+16iXxhRkYA8/8H2nF7ZE8IdJuRpqhBUoPF/8Mt2QBLIlvNIdIzFEy2O7ZhwL9Dt08AG9v5c91QPzjsLok2e+5hFGaeMpGQJCE1V3uHyrOAeyZs1QYr7qBWbjQCVh0Phxz9yOA9RPfnQdjyJTq4QEj6vVYvCi5K/CGp/DnnLnGyYJKZOt7nBx02fTVI136UXqt29eF5NRkCpnUqah2s0OXkijsw5Y9LsbwiUxELKtCthESOwN8e/ZNvn/gjPfZWIaB0gupRNugL8Xo9Cx6vFmaW8wzm1IutJvo1mWvkWMvuYYjvd4aDyP6s7PFnSX7DoD/pfxoQuzjwHO2OD93nCOx0ofnNojLNooeJPKLikiwS4qKc8exWd+TlnccKSkXO8Y8S+XRgeE652YNlf+9DH79lNLeK0N0W2tRExX5JaEyJQCuvK0WZi02kxvjmUHhwLAOv9ueC2UCRs="
|
||||
- secure: "JMv9mkSQBJXvUznXcyvngFaOfd2fHYEQQTH8BnS03pqAL0HkWSheG/R2HFJlJv1VJ9MDMf+wVzwMvU/kzkOT68nIgyFWnLBkT6fw+Mw7t/dG96/nOh7DPuaTVzKS0xRbMhDaqZOA9GW13TVnpVdIw79vbhQM69N9Gj80j8oM1cHgMi+fkJSDU3EN/vMJOGKSB3DTpyqAG/nYyZ38tLib8ic12za/YL3HIu8QRHa3fr37+cyrVKgGebGg++yK34p8lC1W4apiO30drmHVQhKWrWnmdcssdGmVM7NystMGGAzUwsJcmRFJuREv1LXiijDzjIRVduceeRYgPi1KHB5ZdL9vji0gM1eHYZZefhcJb6WgPDbCtbjdlCId4v+1bNfsh+dMmhM+vBZDtDEt3UC3MBeYTmklQT2w9zCXHuPBQigj7W4zLm6GxnAXife0SQMfmr736QBvSLUJWtd2tRgS+dRG/LvWxrP5Urvfgs8iRtEVZLDpRR+bSjvLs1UEKLN13KMKYuVwieHbxJn2kY7d5wmZVBYdaokl0yrTkZZ6J9xIphxM8GXJU1BnMZY9zn+Xq3jm576QYnNYUwCtUjOis00Ct4UVuuFqIqZ7bJL7GT4AHTENAk+9GtXVCo3/jnv161rxgoSdnUG6VmrpnyA9jSpcIvMVcE129oghBJ2+PkU="
|
||||
- secure: "otePbb9W6CAuwzayL0dDAvCryzF2s5HHAIytvMW/xnzOuRMR03lumj9bhg56YCB/nIuTncAjToYmsMh2Acrp1Xcx8/DzqKse1K9nkfJgsB+EYeXmy8pVJnCxhHeLCzUPJjUfRoPBm+5GHAkeoviUyxenwbOFm3/Uhtay7i2ZU8K1FsECqx4FZXVh8rrGHqFGzHNimqDDvFUQM6f9a+BevEio8+/aDooaFfvVqPixDYXpi2+99DB/0hZKwcUpj6pOUUszATkAl0kJdOSWI6E7bQa63i6SfhcBNHfvbDqhpcAGs1TIXHbXwWGfySABKgT9mkq64CY3bc/QZ6HeWe6P5tJmt4mGBxDMOWMYj4qtCskQCCCiY9+sCx61Sj8W5w8exjQvvvFA088cKtbCKAx8FIil4CuYPtIjDQFkiNI2bC6BmkUen7qe4z7dSF2AZPb6CuSsBjoXL8Ezle3e1pRrkR+SYbxMSaZVcQoG8hinTqaXhlS5gXi97ZzRrJWn3tUB6JWEBeEkIfTJmrJolbLYIMcADNhKenrW8k6nV32JkvMsCgMFCIkJsIZeuQ9ZYgSZ3CXPODux/D2/4/gNa+353Fs3DTlDQxRTgoaYi9UDIRrKZAkoELFclxuGSKMCCheevQ8FMCmXfxUocwLSgjlO7g4rTJa9Kggn7VltucXiVWY="
|
||||
- secure: "WKdB+WpZG/7SKpPpgM6DAwzVg0QQuxFMJEZE324pagJ74xv+GlObTLm6kU4SAZf6TuRSChTXDAD4paJXkxpq3LlXjp9aKxPASbG5PgZMnGL6bZ40c/UUqrwVy2HLnXPKSNuDBX1RidGXmH3u4YvufjguNnlOLvHYfMHIRGjiEL4ZvC38GB/3YDmID1zI6w8NzTCpFvxeNdg1qhhOBUKyt+icRUwBu8DfgLidzTrO0j5jo3UeqO/w3E9t3nnRgSiO25mBYwWySm1QBK1VS3F5iG9jo1J6GKMOFbWi3lOBoqnWotpfwID+p1vMNX35phHwmMrtoBYfMTV4A2CvQpGyQhn58qyMoKnhLz+NIOxlHN83qcu65bTMG+7ji0pgUl3jhp3ZPyWUjRYQpoVAg5f3UAnUzJgBrOUC0N60ukJVacT/kvkO10CLfz+0+eefW53r+GCkTi2m8iDezZn1olinpLs+Mt0M4VfQ52RVtq3WB6uxN7RjgtrK5XGi2zVAsvfHp+vqFQ6uu+iioiWNji88cTlMuKheo+FWwozNQzd8+4Vxe9w01mLek57Q0dpXiKWL3vMS4Qc4T9E6UzutllDyg/c61LW6Afcqw3jL4HN0UNG3zFefno47oKrmB13HcxakLEC8sdNfE1kWGvB7jqD0snXsz/8rz7rf0IqnK0kaCAc="
|
||||
- secure: "Lah0Q4bWyEJXTBLZHCNkuEuU5wlDQzLF6aQwW47RyVtyRDYW8uQRQFkbb/3oj4QAAgXl0sOcDFnRaaWmlOStPIIYKKVY8Mk6ufyDCz8inWoPNJoSbdqbAi761HacGUsEfSNDrPNB7fst47UZaX7WNAO3q1QjCCGSEJQVpJE2MDAjcSJhAgLPt3JRFSOSwvqxcDLZI+wF9AM81OWNp6QisAdI4LPUJK3L4M4dLh9+apFRb1OMld++scWah1SB5Qj6tPzMvX96mp1XfWB1fJXuf8Yvks02H6ZcAXubK2HXe3iJZKVqGB67kShNN52P+wg0ZZO4OP4kZ2PXahnB7hhxIfEfWsNVy/w4ww1N6K907BfT6RDmsgNP1ZP8kpq6H4pTnWgAy9WDxjatzbNFAHtMzakGjYJNFxZJVco2FL0ipNQ7htoVAA4sUb+VbRQv4O/oZTLMNnnco19+TFJzuZuS4Rjxj/zX63gXkj/W7wou3hw+NpI/PL2hUXIc5imXSLfVQwri8Nl+6IOjV2gWR/vR1VhQqbLsTF6TZQKNI4lvbRTs1nNeqNMW1lHizI3r9Kernj2noAsxJK7wFTZ64OUkQvSNphfmVow29JYQKXbpxbmRrdnmfmtnMsxUngpx9WsTPhrprt5hIAqiBspHemrs+H3LiIml6IY/3l9bcPlc0gA="
|
||||
- secure: "sNc7kC0CuH/TCZh2WwEN51GcA1fSpcliCHpFB+WX7ieQiRu3xKn2avby/T7vbvX0viXRER59arFGQF4i/dyr2g4tlZLVRYjPeiApfduKZ7Lb+vZGro3cWesfHG6Abk2VcgZZli1IDrgrHH5qAWnA9xNnKvKBL9NDM73Zj92BmlDFVEzadTii8brWvced/YP3jNXEmM5ZIufgpe2yidBB2bLWYJXb3Cf1MvzMG4tqNAtZTrI32q50mokz/uTqp3MRJ+cR8sOI+2+2xSbT0zZGLSRZf96/7FKtE0QIDxdWAe4XdlHq1CluRVk30Ju5BEn0QzoYLryCIuw3JjDl1Yksw4IA5imljZJlOmWa2l6fX0HNxMw+z0R/1d2HARA8BY7/uQKv4guV3Cf3jpsWoKSsM1WxqOqsuEFOoRQ2eQNJEaSuC6+j/vzNoj61pOuG0R9OC2PFcFCZ9fomIrZMse+7M3WIj4+mp7e+JDK8DgVdUlqkBVCx1Ospseb5pm6lDx8F5NbgqZgGXgyoWVpqZnyYOoOutezMoD6MI2wXzJaepV/L3+LD5f6q3DAa/sRAEEsBFGyMHXiPYbziEiy8Hz09Sz3inT5rzS8OLOinwAI2sHiIYHTl340XfWdYz6AlNhYLCGwwmtkntbjOj5UVW06IgBBx44ujpZSUjv7SOrACPGU="
|
||||
- secure: "t9/mC8eSsxXIB2vjcZGtGISxrSY3Yd+XS4+/i1YG1mxSov3R6UdhVl2blLgrvFfVCSo61YMAzJbXKtZlXPzhLDXxpsSlWiatjNrKKo4D2unnlHsyEMJ4wfz8cJ8pKyynP7Kc1ZuaqTSMcEZgk85tlew8Zy/VH6g3Mc/7DvP4SxEDRsP+DdCplZc0vbxHDaV3iC93bwNRfy1UQypwJJ2WQviRema3Umneg8hVl2V35zbaBoy45ubCkUoCbRCDaUyoHA10GrE1OUOLsioar/dj6K2W3EhAtehHLWrUZhhl07rayrrRDQYDTdebifB/MWlvR5FjM/Dr5M4k2ciss4ol3IR5LypRLD+/YBtzeIuqkDbUkaBowY+oUj6OWlzEbzAUrUNa5mnyR2jhr8ivZUeEEWLxljsu8gWq65mzgiZt/u+kVCnMLciUv0//0nrsNsEMI9pau2ZxbcpItFVKdZYXFdmHWG+qPCgMcbgsUM3xqaIc7fNwbk2Aa6erIGqD2VkwWzD/xksweg4lsgQ0N1tXMfoWWKh4Xj/OFom+S+3w9uSx/jQma7nXm+PKQL8dIo7rqza3fz9biq5T6mhwUrqCpFIJv7mrMwaTT1UfOchjiLL3CGeO7Amv7Avmh3fhMPbypF0sws79d7ewZbyv+oSzn+pxlCdzBU1GYx6veApbzhg="
|
||||
- secure: "ljVx3TgpBJ/ylMKDVmXabi9UNi5YvrRM5UBTrRk7XPu1YFYS4FI1GcGlyvYhToc4fKt4jLhX9qU+s/rZY+odO0x/HpmJglMBCrY+QcWOzuyaP1U5dCET+evuqFdEAZIzLQc4VDjL1aQLZh+OG7bjoBClVAan6a+pmW0yxBC6rNtCWTESG4rY3wOeTpoI0Q1gM7gg5Zkj4Z+yLvYeJdoKHijM7C3/R/VVTqUFqArk+Js7Qb2qTqm03SHP0ahRQA8XSfbPebSkJyX9oLbidanBEaQE6sqnp9Qh+8VGcnn7VkSu6oq2+ZXz4xlSMrH2Iv2JXl68Td51LsLo9BxaMCL68ssgTFfXPSrrcLwholNEt1pXk5nhBl1l6MZ1UwUJyBm+AXZp/4sCK9/P0rGa2d1rOcpOz7nobH7BDktqEJkrR6VzkTMx1aOwtF+JSt7SJQ1RrRdm9uKfOZZsnw17+VgVAHo/ttY0C3cRl10oaF1C/IdliDfa5gJdZ2VSZtJxyewqKwGiZrqCRv2fQyIuGsqfHXsyHVL6q1KfVcHjaXBvh0o6xZ6duieFT4FNHg7clv1qPQV+cLh4L11nugiihRTeYQtKyUnP5YIL+jlcGNM6KqKhF9RN5c+zOqWNmEcz6O8nljY5mFWdIxL6dFfE3+4wpw7snFP0PrWIWl5SmrU5ipY="
|
||||
- secure: "L0ivSUmbOHNmKWVR2zeYcVR+Xr6780dYA829MyksXfg9OrieTS+qVqSODKexFW89dp4Wf8xFdT9f16xSqjA/xSuX12uiISMiTcFKP39ZnQZ//NDkx1T4pZaeNiysqT+2Ys9OaKTWxn8luFtAYp+DaluancQtEw6+M8H3jQyQZimGHl0QB6BK2A5VA2vko+7ebIdwu5Df5E7lc/LG42H+DdSFkSj7Meu5XMyPHSZRAdOE5doO++tqFKzgs2WbDRHRUP4R4LqtDlGn8+5qnQtQKUN4V9UGrKM95R34BhgUlgOqOFAjFDug71/1/rRyv9XnzKkAdTIbxV7nmxSL+APhQDqpwPr01EP9gtZBOycswV/igJYotgqkhzwNAYrmwOA5Ta8S18Ck0feDEqT20w8yKS4QwV2Ihg4q7CqDFu7i+9iSRUCd/RVrGtG0U5Zo1b3+1JcNxXH/ErUeyHPfrk4vktxfHmU+omqwkfvTRy5upn0Ycr57YHOJc/Iyur7vE07HcnBfAwV0d5KO6HJ7M1n6hmMxeyKmf+qiyQQnphySvHHAfa/9Sec7omwwKv4bn33DwFGc8GWvL6cZXWhmGhFvd+LslpZl0vZ+7Bz0tXlAg4t8V48y/ZtyoSpny0HP4UstdA43PttvZ6q2y8LUNk4LBP5btKmp27Fszuj/rldtj5g="
|
||||
- secure: "Dox8JthAJqWT9eh3Jt4Morbf4pGN9OjduJXe/lYMsmFvqNg7b94P2QdumWBPmVjDq4YjDVirMejBA/TNwORgKfg7pI7MOw+qqoHpT9xyPecXi3ecyBay13e16p0GNRlc5pUu5JcU8sgCpttvM0EAw6bOuQIhnnkIFesbOvwoxGYjMzjmWMNuikR3CjKbo0LDtD5NJXT1OMSqrRuh8NM/BoKyn/kCdSaq9wI2GUMbkg09/kFJkQOvtMXPkM7dIhr/9UC0ouMIyqe/MHa8O6Y4xESdqiTql9uz1+eZfHIRrgFlHfxDvkMv87Cx5OuL+O+qeT/a+RYLCRJspMoq94IYHGg2iyEfBO2YAkogl53wiEf2KF2JdiNGT0xId7bxCJj3efTuCAXV1oqaHpJli1Mhvs7zPEtf78B4tkWEgjhGr5pBLIlbhNjS5wtTHJX4BUzoiP+wODj4h7rjPAah42nWF8XOMlboVi56sOCLjiHBOvYObqyhSfiQxoi2XHphsrZqw6H03tr4Kqd9HVmuoSvRiv+NOu24Ubr6MrrQM2/G72TrTx0/aBlt8Dx5nx2oWZ9ZMiDUR3XlvDLUi45SpY5qESXz08nRlcdS9EvUpK7C+77bNvX+A3dIhsxnxuNaf2naf+QnYYbvh7q4Qbrj4v6EMYS90Uky1JHdoc2wMua8J+w="
|
||||
- secure: "Is2Lv/rxKKrXnxFns9KQYseD02tjY9qbgSteVtJavG1cLJDvkTwb6X+Thvgo4cxk5fQPiXScrQaYzHjVVuNleD+dyD1HC/8CU2Xq+tjBhPjdcccHFSbk06DpcETmLGRyMORXG9JkYlgXHLLXtu/9icppWEHgra+zvchVL2YDhofYne8FMNBb/lq0AAC2wgzAS33tW1+57HaYnzl0hf6+Q6lwqoH2/aTfGMRFDgyJ0HK+5IVfLnQJ+OuGFSrj8/0FWSggR5+EXDIddDgovFgaCghMjHYp21bzn0eIAJtuNFFultwk/UC1lT9joXTKEzgLTAh+w13yz1T3x9rNuv6FDKCotBIS/ZDtPmgvyZ8xvB4SzyULnTRSVn7YvspKR6PAO/qxGNudUD5H8tRKer4qKnKjHzSUcVBlRHb4yE6FqqY1Z9RLEcomWO43nJwb6saNHR9BYedyi0gA+EbA+P259QFClW1dWEQ2LQhDa+0VRssOqZ0BQblPFyz+e5Vc9kfAMbOuoss8fjkiYv+twXv7nT27xrVT5okfKDSiy5opZD6d36N5FibZPYiMrVx00YZdkFB+5EqQuJ7lqKUMkZJTeApLzj+h+/4aAOWd3paj5ghv7m+9ReohsNKFHyjaSy97RhMAZjzqgMMdD8rjUSKDhvNKvvQECWHlaXwL129GB0s="
|
||||
- secure: "l7vWxfcu1RgXbStq76Mzz2I5Iu4e31729OyLYqulXZzft3wO+idvgQKy/JSwajiKgOxlpBuI0wrncgIUYshcRvE4yB0y9+QIMDTegJzTADtRSUyVNCIZfTgvtOvzrlW0iCdVsxLBtYcJWJVPdjF2q59ED1jahd1AuJJqX5e61gfr0eZ9+cNiHbX1u3VpmGchFNWQF4KvebE4WKs5xWEds+AtbTODdQq3H6kKQK3fTVJnbz6WMO+bEWgWI7orfSE20lku/3Q3eMLhNOcPwH8WnUoTTvDWol5Cq5NfPhkKF5aV7kIbNXkxswM7yBPAPumBiXdM1BHpfd4+0YQ/fqRtnqxw85HEYpT8dRewHimCl4IccgGCR+G0tK8RNleKL28GWrz86gRVpXMEhOU3ILwb8as3SdQWLwhXXy5uKGJmsdrCNAH4/8eu3XczO5VN2wOo7narYuBgGcl2eLW9TOLyNVKFKxbQnDLBiOybLNoOV42DCRYt7v3Eknkv1Z0dmX6n23q1z9if9kkfAFgRQWsTbNZyWeIwWuX8b2a0Zq0znS3JflSKxzqS7RHADfBOJEVM86AiGkw0XkR4o31yDrY+zOrkJ3GxfV/HpqG7LzCd9tFeexanneDCE1FJhCkDjpnc0Mdyx+1gVf+u2MkgrU0BbCf2EESBAlC/FTRvxTmk/6s="
|
||||
- secure: "bvtQ348bxKwTtB8X0zMxeTsM0jEJozbS6/rzH/88Fk90a+KO8SdXen0Kj9/LahV4duMn2xTTRmxMCVj424FbcVTgBkJpIO7btqTcNASORkmK+9wGTK6Bgb9R5sHLbVrbrMYJzsMQR0MWxE8ibPLlyom+ssUIqr7HAjnRyYSDiKChGhgBfdE/0G9OE/DuQaU/ZkTuEYfc4527QNLJ8Bt1aLDdCHJLxzCojNaTHB215Tz7dmpnJjWa45BXxkMwLTpxIJuY7wA7K+2UBJfLvZv5QiPYyeUlosV7FwhCN9e90+dc2x8HPJbwe/Ysv5dm8/FfNTgTe6IkKw1z9kev9/W1tYCtqMzn9GnlIH2000ZUomhHKbc4UUl8sskF2jGr14rSz7pfaTyvXNvK7nKLKM7mZAO/cAaQvZoGEx0s6B7a7YX38NmJWcM0UUD603n4G9uVZNoxjyr8HWC6pho8MY8/u5aFKMPd+C8DaDWeSzNJDZRyi62v+t2iyyZuQUtVXtnDpv8GQW4tYFIJCkkrTm5VNNgD2x3WRsEr3LsBa3BonOmi3a1VrdIpyidDzBEWFUR59a/C0lU5J2UX5W1vu/Pnr9/8p898sDvvDbluz47OR9xoVOjtmGdt/ENJa4EOY1q/eRIN6zrt/cwwjIKgTcYOVk7DFDMXQuYlDeEFuHSJ5Xs="
|
||||
- secure: "I05ZVphCT2Alu4najchCjDLIgflsdSCR/Z20OkMbcESprwJB5h7nb64kuVkqPQT7GeMcXMnK1xahx0KvE1Gn8OqOlDPt5WgnvcYERnVOKEJabmRXws8nM20AehwAzRoc6uDhaTnmwHRuliPSeuxiwY2Rkyj5hdyvEMzaOSY+qoIOYtDYQu0MM1XIC3cAjSbEfEGuqnKcN0p9VG0l7WscEUtcKPSFBShOlJZWDiHy7lisgO6OEzvAeDpZU/1ZdljXBl0RoDX8l4LqH4SzQqE68kK7qqIhZNmx4hkrh7qtx8tVqVKy/R3IUYhdxrJ84eJxrex6RQb9SgJQRGwi/3+rkKxVS41KiQ4quJHJhnqd2A1+5rDMN/fsjbjtM1I3VIl6w4oaT1C1QtwSjQOvuZCKDLIszVutW0PuGHveNyjwqQSm2PboOkLTg0dNrFIflT2hGMynv1s19WzGY8qQDP2UgODNR4EQVBy0PRk43ne5W/AFtzY1wUUUCBfKi7WYi4uURG/1Xu456GEElbIOwx9F1nC0s3z6ZFVJ3bVbmFxUlHtCyL5wJ3fiVHkAoPSygVARvv2UhHmXvJXXjxBMOvsnZZbpjvcbTeZoguffxMZEMNhuI4W22WjLdVTLxi9uh1VTdm8vkfAeNjMPw53gwPaSz7Jv6A82LFqpmbwAHxNAEy8="
|
||||
- secure: "BSZK3v2RYsNKv/8lBZ/dKagYF+znGXIeDY271A2nCS8DXx+y0octwI5RT08+UqdD1AvhPOPjdnSXG+M2NOSLlfBoXF/0t9S39+qy3EhGgxYAcWcVDXnrOrHEOALg1cQkCA2D/CG+eeG/36SmAn6FINTLaAOilxWGGCeL0KOB5mqPPnPogaYhowIwDkj0Hfsfrw8011XzgbufNr15GIYJ+nP7f41NcgIbnNkqXY6Q1jiWs1DuKXNDr5WGPqztHtVf7RtcpVmShzptAcAMdRqCGKF7atCZhXPbd69j3qf7F533/tCSCIrszR/Wp4BKlJfVCRCk1oiEcfRvprJqeFJ+0WJCVzyD7hXfnkBg2Tb5PmzvxOFM+hsqRu4mY3UdjqXukzAe5O0O6Uo8sqzqQUIjUooRnNQ4GdPd+7wMbyBZn4PJaW6YTi/7zg7mAegqot6uyEGGpWb6iFYrf6DX75GUfTciNktv+ez0AeqYFwNBLgcOsmaq+3V+aR+dIxsxIzC/i5GCTHvMYOIp6Q2tPCuIug0tX6uxm++vMOoMLK+vG1fVQ0Cd8m6yQIWEXUu0VBs1MTlJD+vX6LsK8CGo0Dt3ZS3JmC4TGGo9CBSEkeDvnKvQeX6x3NBIFWvgt+Hjxh4S9U+vW+AaUXpV9k+NEhesC/8Ys0UGbGYsTDzHdx9TMBY="
|
||||
- secure: "sskZYJ/+xmHh5GNvw3QRf10+sBGOjlub29jOTxjXIRYUyZfkhjsF8CR9NP59CBFuqgN716Mj1uEVEcx0aaepr/zWweQSnqa9lZgFD9nDYALVNh1b4oyqfhYG1sw57ZHdBsJOBF4mKBnahly/QQj11Ya25GyIS2IewwCWVNtCOJYietSssG9l0+qc+RPG5Ub/lEKA3VRrtbUuApcIYszt33DumgZ4wzkzICl1SuOy8V15vHVlkBqASJheEDa4Hia+eAMo6e7OCE5KWjl7K2MxTvtszwFi7lgMCyPCOy6DkaULACfBnQeloh42B2Qhd/ta04vuRKg6y4fKSrnegpeOAa9aGxF1ufvG9IvPsRTsu4+w63b9xsQUN9MyDgxcJe0YeUHPgPYL6mqAjCIKvL62LlIyrQ8IAteoh3MC+4Xb8crXaLGINTcLwYvLwxsHMuC/58hdQ23I4DqnyZGAS3L1IhpX4QvYN6xc7H+ptaiQttweoH5VpMAduSrmSnNmvLOc4g2PvbRES6D95moQ7qk7iX6vpu+PevL5HtQCbB8SFyFLTYWsqZaG+4NrjpIi2hLzUT35meHwrvxG/MsfeqOVlnBfa93VnX75vxOkRFNY56sOtrTJaiVZ+rQUdszAZz3KGPbyrSlz3KxV+ZKxZH6/0oFteAJttXFjWQdICnKGDSo="
|
||||
- secure: "sdzU5bPs1w7Nzf3F5Gtk/iq2Kfq3zfLQNGcehLZp1gJXzNf7F6HZLOn6GaXQ/5RVlhqR5nOmzMpVQs88rZv+6PE6YqGMugTxHIQeNmXtGnuEDJSBvGT9Ok8ENxcwKwL93g9SCd9P8mTspxklOsW2Bk3No2+Zlc/aQguBcX44TwYF42KuBU4O7oS6pUg8NjnuQp2zTyhp0ouzyAudatPyu1BLci/3lbx+MehutQw1y4Om6g8tfwf950UONZQdqq9MBluu7yYb1oHkdC1J4qgwdCZkJslWIwQHCH5UE4AW9iVG0qVrLpzBGtV27Kfy/Vf8r2gMYzbiur2h2+zzWSDm8/bk8YLn7u1FBpjGJdJ0pX0ZrZr+hMV2vH3e54NvA5WyRU7tw8mZ1PoDYd/FM5KXIYbscSbSRCqTsbPgyVIHuoOWXeeSe+/Ef1ifZv3HHf6ARfUIWupKfAipxChc7QUMI/HEQ9QPsqgBaooZD9chGsWAgv+8tFxdteqkx4Yh+AuZp1rVykB/9vAamUBebQxy0oeGm65j6X1rksfjAPkfeDYB7L4Ruy7tUwPtvYrAHPoWrf9O0g/qDRsw0vdqp42CjszpIxhuPhVRDx0i0wquqw2LnIU3ejRv2R8d1SecVKBBcVsWLFvc9iR4rNIi5JnxITRtiwL1Xv3Vcgx7WwxExtE="
|
||||
- secure: "f9n1KmU5NuV4jGkrhLNiPD+3Cy7t4D7Rq8YsPlmyB2A6u6IHMuOVP95IwH6Zt0cmMDBrZGthj3/0iu5kzxzqD0m135PT17wSEqsDfDMyKRZJQuYSO/ESVxnSca3afRH7Ds7/ipQVd/ljZgwEnW3JMaOQiIdbbIquNOOTeY0/wsXkreDamXZaKKqUoeadkAV4AkKhM0xcMg+Vni1i71TYPBWrZPLVAu3ZrSvU/cE5mtBUIkbr9EgsEE+WR23QCgtwxKzNxrXetBcPXDsJb98/ABgpoItm5Ko/Zk6pkib44f+iQtn7Y6j3lieELCH5Sn0uy3RrMxkl7xicB7zPYME94NEPHCmshyVsH3RxWfcBG4kauRNBCLYLl5HYF2t1lWZ6In5qlx8xN3Tf0KrbM3vzAEOnnfZt83h3q5OuWl+jzTOcv9xHmeW0lnwEEfS0nxUV3KDqLFBczcUxKBmsA2aWnJZ2HV+kls6OaWZ987m6V2pIGR2uviGT7I4ngjCSOJzbwYbvJbJqYAI97FWP/5pqv97xHLCSSJh6TBO4NMQ7Ib/W1XT6NZyPOjNGSLpd79UA3cTzU2+UYr/7RxZpAKONSAMTJh7CX451XQwgovpFZ2quXs2BzqcVpy8AlUc45ygnFOALOANAkcRP5QFhmff0jpXstjPW83/GksbtaiLhIiQ="
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- $HOME/python
|
||||
- $HOME/ssl
|
||||
- $HOME/pybuild
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
name: "Linux 64-bit Xenial"
|
||||
dist: xenial
|
||||
language: bash
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=build
|
||||
- os: linux
|
||||
name: "Linux 64-bit Trusty"
|
||||
dist: trusty
|
||||
language: bash
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=build
|
||||
- os: linux
|
||||
name: "Linux 64-bit Precise"
|
||||
dist: precise
|
||||
language: bash
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=build
|
||||
- os: linux
|
||||
name: "Linux 64-bit Xenial - Python 3.5 Source Testing"
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- "3.5"
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=test
|
||||
- os: linux
|
||||
name: "Linux 64-bit Xenial - Python 3.6 Source Testing"
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=test
|
||||
- os: linux
|
||||
name: "Linux 64-bit Xenial - Python 3.8-dev Source Testing"
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- "3.8-dev"
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=test
|
||||
- os: linux
|
||||
name: "Linux 64-bit Xenial - Python nightly Source Testing"
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- "nightly"
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=test
|
||||
- os: osx
|
||||
name: "MacOS 64-bit"
|
||||
language: generic
|
||||
osx_image: xcode9.2
|
||||
env:
|
||||
- GAMOS=macos
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=build
|
||||
- os: windows
|
||||
name: "Windows 64-bit"
|
||||
language: shell
|
||||
filter_secrets: false
|
||||
env:
|
||||
- GAMOS=windows
|
||||
- PLATFORM=x86_64
|
||||
- VMTYPE=build
|
||||
- os: windows
|
||||
name: "Windows 32-bit"
|
||||
language: shell
|
||||
filter_secrets: false
|
||||
env:
|
||||
- GAMOS=windows
|
||||
- PLATFORM=x86
|
||||
- VMTYPE=build
|
||||
|
||||
before_install:
|
||||
- source src/travis/$TRAVIS_OS_NAME-$PLATFORM-before-install.sh
|
||||
|
||||
install:
|
||||
- source src/travis/$TRAVIS_OS_NAME-$PLATFORM-install.sh
|
||||
|
||||
script:
|
||||
- $gam version extended
|
||||
- $gam version | grep travis # travis should be part of the path (not /tmp or such)
|
||||
# determine which Python version GAM is built with and ensure it's at least build version from above.
|
||||
- if [ "VMTYPE" == "build" ]; then vline=$(gamd version | grep "Python "); python_line=($vline); this_python=${python_line[1]}; tools/a_atleast_b.py $this_python $BUILD_PYTHON_VERSION; fi
|
||||
# determine which OpenSSL version GAM is built with and ensure it's at least build version from above.
|
||||
- if [ "VMTYPE" == "build" ]; then vline=$(gamd version extended | grep "OpenSSL "); openssl_line=($vline); this_openssl=${openssl_line[1]}; tools/a_atleast_b.py $this_openssl $BUILD_OPENSSL_VERSION; fi
|
||||
- if [ "$VMTYPE" == "build" ]; then $gam version extended | grep TLSv1\.[23]; fi # Builds should default TLS 1.2 or 1.3 to Google
|
||||
- if [ "$VMTYPE" == "build" ]; then GAM_TLS_MIN_VERSION=TLSv1_2 $gam version extended location tls-v1-0.badssl.com:1010; [[ $? == 3 ]]; fi # expect fail since server doesn't support our TLS version
|
||||
- export jid="$(cut -d'.' -f2 <<<"$TRAVIS_JOB_NUMBER")"
|
||||
- if [ "$TRAVIS_EVENT_TYPE" != "pull_request" ]; then export e2e=true; fi
|
||||
- if [ "$e2e" = true ]; then export gam_user=gam-travis-$jid@pdl.jaylee.us; fi
|
||||
- if [ "$e2e" = true ]; then openssl aes-256-cbc -K $encrypted_ab10ec38326e_key -iv $encrypted_ab10ec38326e_iv -in travis/oauth2service.json.enc -out $gampath/oauth2service.json -d; fi
|
||||
- if [ "$e2e" = true ]; then cat travis/cfg_template.json | python travis/svars-write.py &> /dev/null; fi
|
||||
- if [ "$e2e" = true ]; then $gam info domain; fi
|
||||
- if [ "$e2e" = true ]; then $gam oauth info; fi
|
||||
- if [ "$e2e" = true ]; then $gam oauth refresh; fi
|
||||
- if [ "$e2e" = true ]; then $gam info user; fi
|
||||
- if [ "$e2e" = true ]; then export tstamp=$(date +%s%3N);
|
||||
export newbase=travis-test-$jid-$tstamp;
|
||||
export newuser=$newbase@pdl.jaylee.us;
|
||||
export newgroup=$newbase-group@pdl.jaylee.us;
|
||||
export newalias=$newbase-alias@pdl.jaylee.us;
|
||||
export newbuilding=$newbase-building;
|
||||
export newresource=$newbase-resource;
|
||||
export GAM_THREADS=5; fi
|
||||
- if [ "$e2e" = true ]; then echo email > sample.csv;
|
||||
for i in {01..20};
|
||||
do echo $newbase-bulkuser-$i >> sample.csv;
|
||||
done; fi
|
||||
- if [ "$e2e" = true ]; then $gam create user $newuser firstname Travis lastname $jid password random travis.jid $jid; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user sendemail recipient $newuser subject "test message $newbase" message "Travis test message"; fi
|
||||
- if [ "$e2e" = true ]; then $gam create group $newgroup name "Travis $jid group" description "This is a description" isarchived true; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $newuser add license gsuitebusiness; fi
|
||||
- if [ "$e2e" = true ]; then $gam update group $newgroup add owner $gam_user; fi
|
||||
- if [ "$e2e" = true ]; then $gam update group $newgroup add member $newuser; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv sample.csv gam create user ~~email~~ firstname "Travis Bulk" lastname ~~email~~ travis.jid $jid; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv sample.csv gam user ~email add license gsuitebusiness; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv sample.csv gam user $gam_user sendemail recipient ~~email~~@pdl.jaylee.us subject "test message $newbase" message "Travis test message"; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv sample.csv gam update group $newgroup add member ~email; fi
|
||||
- if [ "$e2e" = true ]; then $gam info group $newgroup; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user check serviceaccount; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $newuser imap on; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $newuser show imap; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv sample.csv gam user $newuser delegate to ~email; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $newuser show delegates; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user importemail subject "Travis import $newbase" message "This is a test import" labels IMPORTANT,UNREAD,INBOX,STARRED; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user insertemail subject "Travis insert $newbase" file gam.py labels INBOX,UNREAD; fi # yep body is gam code
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user sendemail subject "Travis send $gam_user $newbase" file gam.py recipient admin@pdl.jaylee.us; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user draftemail subject "Travis draft $newbase" message "Draft message test"; fi
|
||||
- if [ "$e2e" = true ]; then $gam users "$gam_user $newbase-bulkuser-01 $newbase-bulkuser-02 $newbase-bulkuser-03" delete messages query in:anywhere maxtodelete 99999 doit; fi
|
||||
- if [ "$e2e" = true ]; then $gam users "$newbase-bulkuser-04 $newbase-bulkuser-05 $newbase-bulkuser-06" trash messages query in:anywhere maxtotrash 99999 doit; fi
|
||||
- if [ "$e2e" = true ]; then $gam users "$newbase-bulkuser-07 $newbase-bulkuser-08 $newbase-bulkuser-09" modify messages query in:anywhere maxtomodify 99999 addlabel IMPORTANT addlabel STARRED doit; fi
|
||||
- if [ "$e2e" = true ]; then $gam create feature name Whiteboard-$newbase; fi
|
||||
- if [ "$e2e" = true ]; then $gam create feature name VC-$newbase; fi
|
||||
- if [ "$e2e" = true ]; then $gam create building "My Building - $newbase" id $newbuilding floors 1,2,3,4,5,6,7,8,9,10,11,12,14,15 description "No 13th floor here..."; fi
|
||||
- if [ "$e2e" = true ]; then $gam create resource $newresource "Resource Calendar $tstamp" capacity 25 features Whiteboard-$newbase,VC-$newbase building $newbuilding floor 15 type Room; fi
|
||||
- if [ "$e2e" = true ]; then $gam info resource $newresource; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $newuser show filelist; fi
|
||||
- if [ "$e2e" = true ]; then $gam calendar $gam_user printacl | $gam csv - gam calendar $gam_user delete id ~id; fi # clear ACLs
|
||||
- if [ "$e2e" = true ]; then $gam calendar $gam_user update read domain; fi
|
||||
- if [ "$e2e" = true ]; then $gam calendar $gam_user update freebusy default; fi
|
||||
- if [ "$e2e" = true ]; then $gam calendar $gam_user add editor $newuser; fi
|
||||
- if [ "$e2e" = true ]; then $gam calendar $gam_user showacl; fi
|
||||
- if [ "$e2e" = true ]; then $gam calendar $gam_user printacl | $gam csv - gam calendar $gam_user delete id ~id; fi
|
||||
- if [ "$e2e" = true ]; then $gam printer register; fi
|
||||
- if [ "$e2e" = true ]; then source travis/set_printer_csv_filter.sh; fi
|
||||
- if [ "$e2e" = true ]; then $gam print printers > printers.csv; fi
|
||||
- if [ "$e2e" = true ]; then unset GAM_CSV_ROW_FILTER; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv printers.csv gam printer ~id add USER $newgroup; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv printers.csv gam printjob ~id submit https://www.github.com/jay0lee/GAM; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv printers.csv gam info printer ~id; fi
|
||||
- if [ "$e2e" = true ]; then $gam print printjobs; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv printers.csv gam printjob ~id fetch; fi
|
||||
- if [ "$e2e" = true ]; then $gam print printjobs | $gam csv - gam printjob ~id delete; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv printers.csv gam delete printer ~id; fi
|
||||
- if [ "$e2e" = true ]; then $gam csv sample.csv gam user ~email add calendar id:$newresource; fi
|
||||
- if [ "$e2e" = true ]; then $gam delete resource $newresource; fi
|
||||
- if [ "$e2e" = true ]; then $gam delete feature Whiteboard-$newbase; fi
|
||||
- if [ "$e2e" = true ]; then $gam delete feature VC-$newbase; fi
|
||||
- if [ "$e2e" = true ]; then $gam delete building $newbuilding; fi
|
||||
- if [ "$e2e" = true ]; then $gam delete group $newgroup; fi
|
||||
- if [ "$e2e" = true ]; then $gam create alias $newalias user $newuser; fi
|
||||
- if [ "$e2e" = true ]; then $gam whatis $newuser; fi
|
||||
- if [ "$e2e" = true ]; then $gam user $gam_user show tokens; fi
|
||||
- if [ "$e2e" = true ]; then $gam delete user $newuser; fi
|
||||
- if [ "$e2e" = true ]; then $gam print users query "travis.jid=$jid" | $gam csv - gam delete user ~primaryEmail; fi
|
||||
|
||||
before_deploy:
|
||||
- export TRAVIS_TAG="preview"
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: bzambMcQwyv/o5c5GrKGCsZHgE5R85tg8sNFvPfpISz3+uosCjnBXas7wvCKzT75XUFi2ztfbYak6HdKf4sGnNHk0saEicB3slH+ghPyZbYzp76yvvduhFO2nWW3/F01tL+Yfqqt4/q8wFaWGjrC5km+6GLVyB4lWA/Uyu49qKnz02uSwyhBD/VFbO7DOQ65a1iWk9HngyMsu0Oi7HIbSjSLtxTHedNfOf3waW0NivTTxYXiYGX/MCu3GWhgIGj47a+H3A6FcQ/9QWvnKgnoixdgPBUz7kDb7ktsWwQsILPGStgH7iMuG49ZlXdEFmqwifBri2wvzmFEevBGZjHcupy1IGrNFRG+IUGKMotio+OkLHlLjuv7ZJtqCz/Vf5SNFgNyMSanx6jKEUJuYvndVg99IRXmYVwHFwPu5BAcJACpU6C0AfyGmmSqqwxCd46uXL62ynxNFpHuRfOqlDnmCTfZgjOciJSlDDpf+Xz9fF7+oCoeCi3mrcZVFjhd3tT6Oxw5HrsDtm0ZNld1cdLidaq8H6vOFgHMd0A9yNYZzTzXTvpmxzkXT4Zc7s+PYKN6z5fRZ+pJeckUjRXblvVEfs5HFSymavcOc5AkRwxpvOsTQMNmlnaJCBo5UNs0K/rVmRi5cFmaiwTcBCY0kTllOBJ4zWsfq8seiokWwNUNK2g=
|
||||
file_glob: true
|
||||
overwrite: true
|
||||
file: gam-$GAMVERSION-*
|
||||
skip_cleanup: true
|
||||
draft: true
|
||||
all_branches: true
|
||||
on:
|
||||
repo: jay0lee/GAM
|
||||
condition: $VMTYPE = build
|
||||
@@ -1,4 +1,4 @@
|
||||
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily.
|
||||
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily. [](https://travis-ci.org/jay0lee/GAM)
|
||||
# Quick Start
|
||||
## Linux / MacOS
|
||||
Open a terminal and run:
|
||||
@@ -12,6 +12,8 @@ Download the MSI Installer from the [GitHub Releases] page. Install the MSI and
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
# Mailing List / Discussion group
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
# IM Room
|
||||
[](https://gitter.im/jay0lee-GAM/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# Author
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
|
||||
@@ -41,14 +41,14 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<FileFormat> ::=
|
||||
csv|html|txt|tsv|jpeg|jpg|png|svg|pdf|rtf|pptx|xlsx|docx|odt|ods|openoffice|ms|microsoft|micro$oft
|
||||
<LabelColorHex> ::=
|
||||
#000000|#076239|#0b804b|#149e60|#16a766|#1a764d|#1c4587|#285bac|
|
||||
#2a9c68|#3c78d8|#3dc789|#41236d|#434343|#43d692|#44b984|#4a86e8|
|
||||
#653e9b|#666666|#68dfa9|#6d9eeb|#822111|#83334c|#89d3b2|#8e63ce|
|
||||
#999999|#a0eac9|#a46a21|#a479e2|#a4c2f4|#aa8831|#ac2b16|#b65775|
|
||||
#b694e8|#b9e4d0|#c6f3de|#c9daf8|#cc3a21|#cccccc|#cf8933|#d0bcf1|
|
||||
#d5ae49|#e07798|#e4d7f5|#e66550|#eaa041|#efa093|#efefef|#f2c960|
|
||||
#f3f3f3|#f691b3|#f6c5be|#f7a7c0|#fad165|#fb4c2f|#fbc8d9|#fcda83|
|
||||
#fcdee8|#fce8b3|#fef1d1|#ffad47|#ffbc6b|#ffd6a2|#ffe6c7|#ffffff
|
||||
#000000|#076239|#0b804b|#149e60|#16a766|#1a764d|#1c4587|#285bac|
|
||||
#2a9c68|#3c78d8|#3dc789|#41236d|#434343|#43d692|#44b984|#4a86e8|
|
||||
#653e9b|#666666|#68dfa9|#6d9eeb|#822111|#83334c|#89d3b2|#8e63ce|
|
||||
#999999|#a0eac9|#a46a21|#a479e2|#a4c2f4|#aa8831|#ac2b16|#b65775|
|
||||
#b694e8|#b9e4d0|#c6f3de|#c9daf8|#cc3a21|#cccccc|#cf8933|#d0bcf1|
|
||||
#d5ae49|#e07798|#e4d7f5|#e66550|#eaa041|#efa093|#efefef|#f2c960|
|
||||
#f3f3f3|#f691b3|#f6c5be|#f7a7c0|#fad165|#fb4c2f|#fbc8d9|#fcda83|
|
||||
#fcdee8|#fce8b3|#fef1d1|#ffad47|#ffbc6b|#ffd6a2|#ffe6c7|#ffffff
|
||||
<Language> ::=
|
||||
ach|af|ag|ak|am|ar|az|be|bem|bg|bn|br|bs|ca|chr|ckb|co|crs|cs|cy|da|de|ee|el|en|en-gb|en-us|eo|es|es-419|et|eu|
|
||||
fa|fi|fo|fr|fr-ca|fy|ga|gaa|gd|gl|gn|gu|ha|haw|he|hi|hr|ht|hu|hy|ia|id|ig|in|is|it|iw|ja|jw|
|
||||
@@ -74,6 +74,13 @@ If an item contains spaces, it should be surrounded by ".
|
||||
Google-Drive-storage|
|
||||
Google-Vault|
|
||||
101001|101005|101031
|
||||
<ProductID> ::=
|
||||
Google-Apps|
|
||||
Google-Chrome-Device-Management|
|
||||
Google-Coordinate|
|
||||
Google-Drive-storage|
|
||||
Google-Vault|
|
||||
101001|101005|101006|101031|101033|101034
|
||||
<SKUID> ::=
|
||||
cloudidentity|identity|1010010001|
|
||||
cloudidentitypremium|identitypremium|1010050001|
|
||||
@@ -81,12 +88,16 @@ If an item contains spaces, it should be surrounded by ".
|
||||
gafb|gafw|basic|gsuitebasic|Google-Apps-For-Business|
|
||||
gafg|gsuitegovernment|gsuitegov|Google-Apps-For-Government|
|
||||
gams|postini|gsuitegams|gsuitepostini|gsuitemessagesecurity|Google-Apps-For-Postini|
|
||||
gal|lite|gsuitelite|Google-Apps-Lite|
|
||||
gau|unlimited|gsuitebusiness|Google-Apps-Unlimited|
|
||||
gae|enterprise|gsuiteenterprise|1010020020|
|
||||
gal|gsl|lite|gsuitelite|Google-Apps-Lite|
|
||||
gau|gsb|unlimited|gsuitebusiness|Google-Apps-Unlimited|
|
||||
gae|gse|enterprise|gsuiteenterprise|1010020020|
|
||||
gsefe|e4e|gsuiteenterpriseeducation|1010310002|
|
||||
gsefes|e4es|gsuiteenterpriseeducationstudent|1010310003|
|
||||
gsbau|businessarchived|gsuitebusinessarchived|
|
||||
gseau|enterprisearchived|gsuiteenterprisearchived|
|
||||
chrome|cdm|googlechromedevicemanagement|Google-Chrome-Device-Management|
|
||||
coordinate|googlecoordinate|Google-Coordinate|
|
||||
d4e|driveenterprise|drive4enterprise|
|
||||
drive20gb|20gb|googledrivestorage20gb|Google-Drive-storage-20GB|
|
||||
drive50gb|50gb|googledrivestorage50gb|Google-Drive-storage-50GB|
|
||||
drive200gb|200gb|googledrivestorage200gb|Google-Drive-storage-200GB|
|
||||
@@ -116,14 +127,12 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<MilliSeconds> ::= <Digit><Digit><Digit>
|
||||
<Date> ::=
|
||||
<Year>-<Month>-<Day> |
|
||||
(+|-)<Number>(d|w)
|
||||
<DateTime> ::=
|
||||
<Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute> |
|
||||
(+|-)<Number>(m|h|d|w)
|
||||
(+|-)<Number>(d|w|y)
|
||||
<Time> ::=
|
||||
<Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute>:<Second>[.<MilliSeconds>](Z|(+|-(<Hour>:<Minute>))) |
|
||||
(+|-)<Number>(m|h|d|w)
|
||||
<RegularExpression> ::= <Python Regular Expression, see: https://docs.python.org/2/library/re.html>
|
||||
<ProjectID> ::= <String> # Must match this Python Regular Expression: [a-z][a-z0-9-]{4,28}[a-z0-9]
|
||||
<Tag> ::= <String>
|
||||
<UniqueID> ::= uid:<String>
|
||||
|
||||
@@ -134,8 +143,10 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<ASPID> ::= <String>
|
||||
<BuildingID> ::= <String>|id:<String>
|
||||
<CalendarACLRole> ::= editor|freebusy|freebusyreader|owner|reader|writer
|
||||
<CalendarACLRuleID> ::= user:<EmailAddress>|group:<EmailAddress>|domain:<DomainName>|default
|
||||
<CalendarColorIndex> ::= <Number in range 1-24>
|
||||
<CalendarItem> ::= <EmailAddress>|<String>
|
||||
<ChatRoom> ::= <String>
|
||||
<ClientID> ::= <String>
|
||||
<ColorValue> ::= <ColorName>|<ColorHex>
|
||||
<CollaboratorItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
@@ -144,10 +155,9 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<CourseParticipantType> ::= teacher|teachers|student|students
|
||||
<CourseState> ::= active|archived|provisioned|declined
|
||||
<CrOSID> ::= <String>
|
||||
<CrOSItem> ::= <CrOSID>|(query:<QueryCrOS>)|(query:orgunitpath:<OrgUnitPath>)
|
||||
<CustomerID> ::= <String>
|
||||
<DomainAlias> ::= <String>
|
||||
<DriveFileACLRole> ::= commenter|editor|organizer|owner|reader|writer
|
||||
<DriveFileACLRole> ::= commenter|contentmanager|editor|fileorganizer|organizer|owner|reader|writer
|
||||
<DriveFileID> ::= <String>
|
||||
<DriveFileURL> ::= https://docs.google.com/a/<DomainName>/document/d/<DriveFileID>/<String>
|
||||
<DriveFileItem> ::= <DriveFileID>|<DriveFileURL>
|
||||
@@ -157,6 +167,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<EmailItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<EventColorIndex> ::= <Number in range 1-11>
|
||||
<EventID> ::= <String>
|
||||
<ExportItem> ::= <UniqueID>|<String>
|
||||
<FeatureName> ::= <String>
|
||||
<FieldName> ::= <String>
|
||||
<FileName> ::= <String>
|
||||
@@ -209,7 +220,9 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<RoleAssignmentID> ::= <String>
|
||||
<SchemaName> ::= <String>
|
||||
<Section> ::= <String>
|
||||
<SerialNumber> ::= <String>
|
||||
<S/MIMEID> ::= <String>
|
||||
<SMTPHostName> ::= <String>
|
||||
<StudentItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<TeamDriveID> ::= <String>
|
||||
<Timezone> ::= <String>
|
||||
@@ -217,6 +230,7 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<URI> ::= <String>
|
||||
<URL> ::= <String>
|
||||
<UserItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<UserName> ::= <<String>
|
||||
|
||||
<CourseFieldName> ::=
|
||||
alternatelink|
|
||||
@@ -244,8 +258,10 @@ If an item contains spaces, it should be surrounded by ".
|
||||
annotatedlocation|location|
|
||||
annotateduser|user|
|
||||
bootmode|
|
||||
cpustatusreports|
|
||||
devicefiles|
|
||||
deviceid|
|
||||
diskvolumereports|
|
||||
ethernetmacaddress|
|
||||
firmwareversion|
|
||||
lastenrollmenttime|
|
||||
@@ -263,8 +279,18 @@ If an item contains spaces, it should be surrounded by ".
|
||||
status|
|
||||
supportenddate|
|
||||
tpmversioninfo|
|
||||
systemramtotal|
|
||||
systemramfreereports|
|
||||
willautorenew
|
||||
|
||||
<CrOSListFieldName> ::=
|
||||
activetimeranges|timeranges|
|
||||
cpustatusreports|
|
||||
devicefiles|
|
||||
diskvolumereports|
|
||||
recentusers|
|
||||
systemramfreereports
|
||||
|
||||
<CrOSOrderByFieldName> ::=
|
||||
lastsync|location|notes|serialnumber|status|supportenddate|user
|
||||
|
||||
@@ -336,46 +362,55 @@ If an item contains spaces, it should be surrounded by ".
|
||||
admincreated|
|
||||
aliases|
|
||||
allowexternalmembers|
|
||||
allowgooglecommunication|
|
||||
allowwebposting|
|
||||
archiveonly|
|
||||
collaborative|
|
||||
customfootertext|
|
||||
customreplyto|
|
||||
customrolesenabledforsettingstobemerged|
|
||||
defaultmessagedenynotificationtext|
|
||||
description|
|
||||
directmemberscount|
|
||||
email|
|
||||
favoriterepliesontop|
|
||||
enablecollaborativeinbox|collaborative|
|
||||
id|
|
||||
includecustomfooter|
|
||||
includeinglobaladdresslist|gal|
|
||||
isarchived|
|
||||
maxmessagebytes|
|
||||
memberscanpostasthegroup|
|
||||
messagedisplayfont|
|
||||
messagemoderationlevel|
|
||||
name
|
||||
name|
|
||||
primarylanguage|
|
||||
replyto|
|
||||
sendmessagedenynotification|
|
||||
showingroupdirectory|
|
||||
spammoderationlevel|
|
||||
whocanadd|
|
||||
whocanaddreferences|
|
||||
whocanapprovemessages|
|
||||
whocanassigntopics|
|
||||
whocanassistcontent|
|
||||
whocancontactowner|
|
||||
whocandeleteanypost|
|
||||
whocandeletetopics|
|
||||
whocandiscovergroup|
|
||||
whocanenterfreeformtags|
|
||||
whocanhideabuse|
|
||||
whocaninvite|
|
||||
whocanjoin|
|
||||
whocanleavegroup|
|
||||
whocanlocktopics|
|
||||
whocanmaketopicssticky|
|
||||
whocanmarkduplicate|
|
||||
whocanmarkfavoritereplyonanytopic|
|
||||
whocanmarkfavoritereplyonowntopic|
|
||||
whocanmarknoresponseneeded|
|
||||
whocanmoderatecontent|
|
||||
whocanmodifytagsandcategories|
|
||||
whocanmovetopicsin|
|
||||
whocanmovetopicsout|
|
||||
whocanpostannouncements|
|
||||
whocanpostmessage|
|
||||
whocantaketopics|
|
||||
whocanunassigntopic|
|
||||
whocanunmarkfavoritereplyonanytopic
|
||||
whocanunmarkfavoritereplyonanytopic|
|
||||
whocanviewgroup|
|
||||
whocanviewmembership
|
||||
|
||||
@@ -386,8 +421,8 @@ If an item contains spaces, it should be surrounded by ".
|
||||
<MembersFieldName> ::=
|
||||
email|
|
||||
id|
|
||||
name|
|
||||
role|
|
||||
status|
|
||||
type
|
||||
|
||||
<MobileFieldName> ::=
|
||||
@@ -512,12 +547,13 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
<ACLList> ::= "<ACLScope>(,<ACLScope>)*"
|
||||
<ASPIDList> ::= "<ASPID>(,<ASPID>)*"
|
||||
<CalendarList> ::= "<CalendarItem>(,<CalendarItem>)*"
|
||||
<ChatRoomList> ::= "<ChatRoom>(,<ChatRoom>)*"
|
||||
<CollaboratorItemList> ::= "<CollaboratorItem>(,<CollaboratorItem>)*"
|
||||
<CourseAliasList> ::= "<CourseAlias>(,<CourseAlias>)*"
|
||||
<CourseIDList> ::= "<CourseID>(,<CourseID>)*"
|
||||
<CourseStateList> ::= "<CourseState>(,<CourseState>)*"
|
||||
<CrOSFieldNameList> ::= "<CrOSFieldName>(,<CrOSFieldName>)*"
|
||||
<CrOSList> ::= "<CrOSID>(,<CrOSID>)*"
|
||||
<CrOSIDList> ::= "<CrOSID>(,<CrOSID>)*"
|
||||
<DriveFileList> ::= "<DriveFileItem>(,<DriveFileItem>)*"
|
||||
<EmailAddressList> ::= "<EmailAddress>(,<EmailAddress>)*"
|
||||
<EmailItemList> ::= "<EmailItem>(,<EmailItem>)*"
|
||||
@@ -546,6 +582,8 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
<ResourceIDList> ::= "<ResourceID>(,<ResourceID>)*"
|
||||
<SKUIDList> ="<SKUID>(,<SKUID>)*"
|
||||
<SchemaNameList> ::= "<SchemaName>(,<SchemaName>)*"
|
||||
<SerialNumberList> ::= "<SerialNumber>(,<SerialNumber>)*"
|
||||
<TeamDriveIDList> ::= "<TeamDriveID>(,<TeamDriveID>)*"
|
||||
<UserFieldNameList> ::= "<UserFieldName>(,<UserFieldName>)*"
|
||||
<UserList> ::= "<UserItem>(,<UserItem>)*"
|
||||
|
||||
@@ -553,9 +591,14 @@ Items, separated by spaces, with spaces, commas or single quotes in the items th
|
||||
|
||||
Specify a collection of ChromeOS devices by directly specifying them
|
||||
|
||||
<CrOSEntity> ::=
|
||||
<CrOSIDList> | (cros_sn <SerialNumberList>) |
|
||||
(query:<QueryCrOS>)|(query:orgunitpath:<OrgUnitPath>)|(query <QueryCrOS>)
|
||||
|
||||
<CrOSTypeEntity> ::=
|
||||
(all cros)|
|
||||
(cros <CrOSList>)|
|
||||
(cros <CrOSIDList>)|
|
||||
(cros_sn <SerialNumberList>)|
|
||||
(crosfile <FileName>)|
|
||||
(croscsvfile <FileName>:<FieldName>)|
|
||||
(crosquery <QueryCrOS>)|
|
||||
@@ -569,9 +612,13 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(all users)|
|
||||
(user <UserItem>)|
|
||||
(users <UserList>)|
|
||||
(group <GroupItem)|
|
||||
(ou|org <OrgUnitPath)|
|
||||
(group|group_ns|group_susp <GroupItem)|
|
||||
(ou|org <OrgUnitPath>)|
|
||||
(ou_ns|org_ns <OrgUnitPath>)|
|
||||
(ou_susp|org_susp <OrgUnitPath>)|
|
||||
(ou_and_children|ou_and_child <OrgUnitPath>)|
|
||||
(ou_and_children_ns|ou_and_child_ns <OrgUnitPath>)|
|
||||
(ou_and_children_susp|ou_and_child_susp <OrgUnitPath>)|
|
||||
(courseparticipants <CourseID>)|
|
||||
(students <CourseID>)|
|
||||
(teachers <CourseID>)|
|
||||
@@ -597,7 +644,7 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(notification clear|(email|sms eventcreation|eventchange|eventcancellation|eventresponse|agenda))
|
||||
|
||||
<CalendarSettings> ::=
|
||||
(summary <String>)|(description <String>)|(location <String>)|(timezone <String>)
|
||||
(summary <String>)|(description <String>)|(location <String>)|(timezone <TimeZone>)
|
||||
|
||||
<CourseAttributes> ::=
|
||||
(description <String>)|
|
||||
@@ -625,59 +672,70 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|(restricted|restrict <Boolean>)|(starred|star <Boolean>)|(trashed|trash <Boolean>)|(viewed|view <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype <MimeType>)|
|
||||
(parentid <DriveFolderID>)|(parentname <DriveFolderName>)|(anyownerparentname <DriveFolderName>)|writerscantshare
|
||||
<EventAttributes> ::=
|
||||
(anyonecanaddself)|(guestscantinviteothers)|(guestscantseeothers)|(notifyattendees)|(available)|(visibility default|public|prvate)|(tentative)|
|
||||
(attendee <EmailAddress>)|(optionalattendee <EmailAddress>)|
|
||||
(description <String>)|(summary <String>)|(location <String>)|(id <String>)|
|
||||
(source <String> <URL>)|(privateproperty <PropertyKey> <PropertyValue>)|(sharedproperty <PropertyKey> <PropertyValue>)|
|
||||
(recurrence <RRULE, EXRULE, RDATE and EXDATE line>)|
|
||||
(start allday <Date>)|(start <Time>)|(end allday <Date>)|(end <Time>)|(timezone <Timezone>)|
|
||||
(noreminders|(reminder <Number> email|popup|sms))|
|
||||
(colorindex|colorid <EventColorIndex>)
|
||||
|
||||
<GroupAttributes> ::=
|
||||
<GroupSettingsAttribute> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
(allowgooglecommunication <Boolean>)|
|
||||
(allowwebposting <Boolean>)|
|
||||
(archiveonly <Boolean>)|
|
||||
(collaborative (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(customfootertext <String>)|
|
||||
(customreplyto <EmailAddress>)|
|
||||
(defaultmessagedenynotificationtext <String>)|
|
||||
(description <String>)|
|
||||
(favoriterepliesontop <Boolean>)|
|
||||
(gal|includeInGlobalAddressList <Boolean>)|
|
||||
(enablecollaborativeinbox|collaborative <Boolean>)|
|
||||
(includeinglobaladdresslist|gal <Boolean>)|
|
||||
(includecustomfooter <Boolean>)|
|
||||
(isarchived <Boolean>)|
|
||||
(maxmessagebytes <ByteCount>)|
|
||||
(memberscanpostasthegroup <Boolean>)|
|
||||
(messagedisplayfont DEFAULT_FONT|FIXED_WIDTH_FONT)|
|
||||
(messagemoderationlevel MODERATE_ALL_MESSAGES|MODERATE_NON_MEMBERS|MODERATE_NEW_MEMBERS|MODERATE_NONE)|
|
||||
(messagemoderationlevel moderate_all_messages|moderate_non_members|moderate_new_members|moderate_none)|
|
||||
(name <String>)|
|
||||
(primarylanguage <Language>)|
|
||||
(replyto REPLY_TO_CUSTOM|REPLY_TO_SENDER|REPLY_TO_LIST|REPLY_TO_OWNER|REPLY_TO_IGNORE|REPLY_TO_MANAGERS)|
|
||||
(replyto reply_to_custom|reply_to_sender|reply_to_list|reply_to_owner|reply_to_ignore|reply_to_managers)|
|
||||
(sendmessagedenynotification <Boolean>)|
|
||||
(showingroupdirectory <Boolean>)|
|
||||
(spammoderationlevel ALLOW|MODERATE|SILENTLY_MODERATE|REJECT)|
|
||||
(whocanadd ALL_MEMBERS_CAN_ADD|ALL_MANAGERS_CAN_ADD|NONE_CAN_ADD)|
|
||||
(whocanaddreferences (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanassigntopics (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocancontactowner ANYONE_CAN_CONTACT|ALL_IN_DOMAIN_CAN_CONTACT|ALL_MEMBERS_CAN_CONTACT|ALL_MANAGERS_CAN_CONTACT)|
|
||||
(whocanenterfreeformtags (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocaninvite ALL_MEMBERS_CAN_INVITE|ALL_MANAGERS_CAN_INVITE|NONE_CAN_INVITE)|
|
||||
(whocanjoin ANYONE_CAN_JOIN|ALL_IN_DOMAIN_CAN_JOIN|INVITED_CAN_JOIN|CAN_REQUEST_TO_JOIN)|
|
||||
(whocanleavegroup ALL_MANAGERS_CAN_LEAVE|ALL_MEMBERS_CAN_LEAVE|NONE_CAN_LEAVE)|
|
||||
(whocanmarkduplicate (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanmarkfavoritereplyonanytopic (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanmarkfavoritereplyonowntopic (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanmarknoresponseneeded (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanmodifytagsandcategories (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanpostmessage NONE_CAN_POST|ALL_MANAGERS_CAN_POST|ALL_MEMBERS_CAN_POST|ALL_IN_DOMAIN_CAN_POST|ANYONE_CAN_POST)|
|
||||
(whocantaketopics (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanunassigntopic (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanunmarkfavoritereplyonanytopic (members|all_members)|(managers|owners_and_managers)|(managers_only)|(owners|owners_only)|none)|
|
||||
(whocanviewgroup ANYONE_CAN_VIEW|ALL_IN_DOMAIN_CAN_VIEW|ALL_MEMBERS_CAN_VIEW|ALL_MANAGERS_CAN_VIEW)|
|
||||
(whocanviewmembership ALL_IN_DOMAIN_CAN_VIEW|ALL_MEMBERS_CAN_VIEW|ALL_MANAGERS_CAN_VIEW)
|
||||
(spammoderationlevel allow|moderate|silently_moderate|reject)|
|
||||
(whocanadd all_members_can_add|all_managers_can_add|all_owners_can_add|none_can_add)|
|
||||
(whocancontactowner anyone_can_contact|all_in_domain_can_contact|all_members_can_contact|all_managers_can_contact)|
|
||||
(whocanjoin anyone_can_join|all_in_domain_can_join|invited_can_join|can_request_to_join)|
|
||||
(whocanleavegroup all_members_can_leave|all_managers_can_leave|all_owners_can_leave|none_can_leave)|
|
||||
(whocanpostmessage none_can_post|all_managers_can_post|all_members_can_post|all_owners_can_post|all_in_domain_can_post|anyone_can_post)|
|
||||
(whocanviewgroup anyone_can_view|all_in_domain_can_view|all_members_can_view|all_managers_can_view|all_owners_can_view)|
|
||||
(whocanviewmembership all_in_domain_can_view|all_members_can_view|all_managers_can_view|all_owners_can_view)
|
||||
<GroupWhoCanDiscoverGroupAttribute> ::=
|
||||
(whocandiscovergroup allmemberscandiscover|allindomaincandiscover|anyonecandiscover)|
|
||||
(showingroupdirectory <Boolean>)
|
||||
<GroupWhoCanAssistContentAttribute> ::=
|
||||
(whocanassistcontent all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanassigntopics all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanenterfreeformtags all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanhideabuse all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanmaketopicssticky all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanmarkduplicate all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanmarkfavoritereplyonanytopic all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanmarknoresponseneeded all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanmodifytagsandcategories all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocantaketopics all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanunassigntopic all_members|owners_and_managers|managers_only|owners_only|none)|
|
||||
(whocanunmarkfavoritereplyonanytopic all_members|owners_and_managers|managers_only|owners_only|none)
|
||||
<GroupWhoCanModerateContentAttribute> ::=
|
||||
(whocanmoderatecontent all_members|owners_and_managers|owners_only|none)|
|
||||
(whocanapprovemessages all_members|owners_and_managers|owners_only|none)|
|
||||
(whocandeleteanypost all_members|owners_and_managers|owners_only|none)|
|
||||
(whocandeletetopics all_members|owners_and_managers|owners_only|none)|
|
||||
(whocanlocktopics all_members|owners_and_managers|owners_only|none)|
|
||||
(whocanmovetopicsin all_members|owners_and_managers|owners_only|none)|
|
||||
(whocanmovetopicsout all_members|owners_and_managers|owners_only|none)|
|
||||
(whocanpostannouncements all_members|owners_and_managers|owners_only|none)
|
||||
<GroupWhoCanModerateMembersAttribute> ::=
|
||||
(whocanmoderatemembers all_members|owners_and_managers|owners_only|none)|
|
||||
(whocanadd all_members_can_add|all_managers_can_add|none_can_add)|
|
||||
(whocanapprovemembers all_members_can_approve|all_managers_can_approve|all_owners_can_approve|none_can_approve)|
|
||||
(whocanbanusers all_members|owners_and_managers|owners_only|none)|
|
||||
(whocaninvite all_members_can_invite|all_managers_can_invite|all_owners_can_invite|none_can_invite)|
|
||||
(whocanmodifymembers all_members|owners_and_managers|owners_only|none)
|
||||
<GroupAttribute> ::=
|
||||
<GroupSettingsAttribute>|
|
||||
<GroupWhoCanDiscoverGroupAttribute>|
|
||||
<GroupWhoCanAssistContentAttribute>|
|
||||
<GroupWhoCanModerateContentAttribute>|
|
||||
<GroupWhoCanModerateMembersAttribute>
|
||||
|
||||
<MobileAction> ::=
|
||||
admin_remote_wipe|wipe|admin_account_wipe|accountwipe|wipeaccount|approve|block|cancel_remote_wipe_then_activate|cancel_remote_wipe_then_block
|
||||
@@ -703,41 +761,45 @@ Specify a collection of Users by directly specifying them or by specifiying item
|
||||
<SchemaFieldDefinition> ::=
|
||||
field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
|
||||
|
||||
<UserAttributes> ::=
|
||||
(address clear|(type work|home|other|(custom <String>) [unstructured|formatted <String>] [pobox <String>] [extendedaddress <String>] [streetaddress <String>]
|
||||
[locality <String>] [region <String>] [postalcode <String>] [country <String>] [countrycode <String>] notprimary|primary))|
|
||||
(admin <Boolean>)|
|
||||
<UserBasicAttribute> ::=
|
||||
(agreed2terms|agreedtoterms <Boolean>)|
|
||||
(changepassword|changepasswordatnextlogin <Boolean>)|
|
||||
(crypt|sha|sha1|sha-1|md5|nohash)|
|
||||
(customerid <String>)|
|
||||
(email|primaryemail|username <EmailAddress>)|
|
||||
(otheremail clear|(work|home|other|<String> <String>))|
|
||||
(externalid clear|(account|customer|login_id|network|organization|<String> <String>))|
|
||||
(firstname|givenname <String>)|
|
||||
(gal|includeinglobaladdresslist <Boolean>)|
|
||||
(gender clear|(female|male|unknown|(other <String>) [addressmeas <String>]))|
|
||||
(im clear|(type work|home|other|(custom <String>) protocol aim|gtalk|icq|jabber|msn|net_meeting|qq|skype|yahoo|(custom_protocol <String>) <String> [notprimary|primary]))|
|
||||
(ipwhitelisted <Boolean>)|
|
||||
(keyword clear|(occupation|outlook|(custom <string>) <String>))|
|
||||
(language clear|<LanguageList>)|
|
||||
(lastname|familyname <String>)|
|
||||
(location clear|(type default|desk|<String> area <String> [building|buildingid <BuildingID>] [floor|floorname <FloorName>] [section|floorsection <String>] [desk|deskcode <String>] endlocation))|
|
||||
(note clear|([text_plain|text_html] <String>|(file <FileName> [charset <Charset>])))|
|
||||
(organization clear|([type domain_only|school|unknown|work] [customtype <String>] [name <String>] [title <String>] [department <String>] [symbol <String>]
|
||||
[costcenter <String>] [location <String>] [description <String>] [domain <String>] notprimary|primary))|
|
||||
(note clear|([text_html|text_plain] <String>|(file <FileName> [charset <Charset>])))|
|
||||
(org|ou|orgunitpath <OrgUnitPath>)
|
||||
(password random|<Password>)|
|
||||
(phone clear|([type work|home|other|work_fax|home_fax|other_fax|main|company_main|assistant|mobile|work_mobile|pager|work_pager|car|radio|callback|isdn|telex|tty_tdd|grand_central|(custom <String>)]
|
||||
[value <String>] notprimary|primary))|
|
||||
(posix clear|(username <String> uid <Integer> gid <Integer> [system|systemid <String>] [home|homedirectory <String>] [shell <String>] [gecos <String>] [primary <Boolean>] endposix))|
|
||||
(relation clear|(spouse|child|mother|father|parent|brother|sister|friend|relative|domestic_partner|manager|dotted-line_manager|assistant|admin_assistant|exec_assistant|referred_by|partner|<String> <String>))|
|
||||
(sshkeys clear|(key <String> [expires <Integer>] endssh))|
|
||||
(suspended <Boolean>)|
|
||||
(website clear|(home_page|blog|profile|work|home|other|ftp|reservations|app_install_page|<String> <URL> [notprimary|primary]))|
|
||||
(<SchemaName>.<FieldName> [multivalued|multivalue|value|multinonempty [type work|home|other|(custom <String>)]] <String>)
|
||||
(<SchemaName>.<FieldName> [multivalued|multivalue|value|multinonempty [type home|other|work|(custom <String>)]] <String>)
|
||||
<UserMultiAttributes> ::=
|
||||
(address clear|(type home|other|work|(custom <String>) [unstructured|formatted <String>] [pobox <String>] [extendedaddress <String>] [streetaddress <String>]
|
||||
[locality <String>] [region <String>] [postalcode <String>] [country <String>] [countrycode <String>] notprimary|primary))|
|
||||
(otheremail clear|(home|other|work|<String> <String>))|
|
||||
(externalid clear|(account|customer|login_id|network|organization|<String> <String>))|
|
||||
(im clear|(type home|other|work|(custom <String>) protocol aim|gtalk|icq|jabber|msn|net_meeting|qq|skype|yahoo|(custom_protocol <String>) <String> [notprimary|primary]))|
|
||||
(keyword clear|(mission|occupation|outlook|(custom <string>) <String>))|
|
||||
(location clear|(type default|desk|<String> area <String> [building|buildingid <String>] [floor|floorname <String>] [section|floorsection <String>] [desk|deskcode <String>] endlocation))|
|
||||
(organization clear|([type domain_only|school|unknown|work] [customtype <String>] [name <String>] [title <String>] [department <String>] [symbol <String>]
|
||||
[costcenter <String>] [location <String>] [description <String>] [domain <String>] [fulltimeequivalent <Integer>] notprimary|primary))|
|
||||
(phone clear|([type assistant|callback|car|company_main|grand_central|home|home_fax|isdn|main|mobile|other|other_fax|pager|radio|telex|tty_tdd|work|work_fax|work_mobile|work_pager|(custom <String>)]
|
||||
[value <String>] notprimary|primary))|
|
||||
(posix clear|(username <String> uid <Integer> gid <Integer> [system|systemid <String>] [home|homedirectory <String>] [shell <String>]
|
||||
[gecos <String>] [os|operatingSystemType linux|unspecified|windows] [primary <Boolean>] endposix))|
|
||||
(relation clear|(admin_assistant|assistant|brother|child|domestic_partner|dotted-line_manager|exec_assistant|father|friend|manager|mother|parent|partner|referred_by|relative|sister|spouse|<String> <String>))|
|
||||
(sshkeys clear|(key <String> [expires <Integer>] endssh))|
|
||||
(website clear|(app_install_page|blog|ftp|home|home_page|other|profile|reservations|resume|work|<String> <URL> [notprimary|primary]))
|
||||
<UserAttribute> ::=
|
||||
<UserBasicAttribute>|
|
||||
<UserMultiAttribute>
|
||||
|
||||
gam version [check] [simple]
|
||||
gam version [check|checkrc|simple|extended] [location <HostName>]
|
||||
gam help
|
||||
|
||||
gam batch <FileName>|- [charset <Charset>]
|
||||
@@ -750,12 +812,17 @@ An argument containing instances of ~~xxx~~ has xxx replaced by the value of fie
|
||||
Example: gam csv Users.csv gam update user "~primaryEmail" address type work unstructured "~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~"
|
||||
Each user (~primaryEmail, e.g. foo@bar.com) would have their work address updated
|
||||
|
||||
gam create project [<EmailAddress>]
|
||||
gam update project [<EmailAddress>]
|
||||
gam create project [<EmailAddress>] [<ProjectID>]
|
||||
gam use project [<EmailAddress>] [<ProjectID>]
|
||||
gam update project [<EmailAddress>] [gam|<ProjectID>|(filter <String>)]
|
||||
gam delete project [<EmailAddress>] [gam|<ProjectID>|(filter <String>)]
|
||||
gam show projects [<EmailAddress>] [all|gam|<ProjectID>|(filter <String>)]
|
||||
gam print projects [<EmailAddress>] [all|gam|<ProjectID>|(filter <String>)] [todrive]
|
||||
|
||||
gam oauth|oauth2 create|request [<EmailAddress>]
|
||||
gam oauth|oauth2 delete|revoke
|
||||
gam oauth|oauth2 info|verify [<AccessToken>]
|
||||
gam oauth|oauth2 info|verify [accesstoken <AccessToken>] [idtoken <IDToken>] [showsecret]
|
||||
gam oauth|oauth2 refresh
|
||||
|
||||
gam <UserTypeEntity> check serviceaccount
|
||||
|
||||
@@ -799,14 +866,13 @@ gam info resoldsubscriptions <CustomerID> [customer_auth_token <String>]
|
||||
device_management|
|
||||
drive|
|
||||
gmail|
|
||||
gplus|
|
||||
meet|
|
||||
mobile|
|
||||
sites
|
||||
<ReportsAppList> ::= "<ReportsApp>(,<ReportsApp>)*"
|
||||
|
||||
gam report users|user [todrive] [date <Date>] [fulldatarequired all|<ReportsAppList>]
|
||||
[(user all|<UserItem>)] [filter|filters <String>] [fields|parameters <String>]
|
||||
[(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath>)] [filter|filters <String>] [fields|parameters <String>]
|
||||
gam report customers|customer|domain [todrive] [date <Date>] [fulldatarequired all|<ReportsAppList>]
|
||||
[fields|parameters <String>]
|
||||
gam report admin|calendar|calendars|drive|docs|doc|groups|group|logins|login|mobile|tokens|token [todrive]
|
||||
@@ -847,9 +913,12 @@ gam update customer <CustomerAttributes>*
|
||||
|
||||
gam info customer
|
||||
|
||||
<DataTransferService> ::= googledrive|gdrive|drive|"drive and docs"|calendar|gplus|google+|googleplus
|
||||
<DataTransferService> ::=
|
||||
calendar|
|
||||
googledrive|gdrive|drive|"drive and docs"
|
||||
<DataTransferServiceList> ::= "<DataTransferService>(,<DataTransferService>)*"
|
||||
|
||||
gam create datatransfer|transfer <OldOwnerID> <DataTransferService> <NewOwnerID> (<ParameterKey> <ParameterValue>)*
|
||||
gam create datatransfer|transfer <OldOwnerID> <DataTransferServiceList> <NewOwnerID> (<ParameterKey> <ParameterValue>)*
|
||||
gam info datatransfer|transfer <TransferID>
|
||||
gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>] [status <String>]
|
||||
|
||||
@@ -859,7 +928,7 @@ gam create org|ou <Name> [description <String>] [parent <OrgUnitPath>] [inherit|
|
||||
gam update org|ou <OrgUnitPath> [name <Name>] [description <String>] [parent <OrgUnitPath>] [inherit|noinherit]
|
||||
gam update org|ou <OrgUnitPath> add|move <CrOSTypeEntity>|<UserTypeEntity>
|
||||
gam delete org|ou <OrgUnitPath>
|
||||
gam info org|ou <OrgUnitPath> [nousers] [children|child]
|
||||
gam info org|ou <OrgUnitPath> [nousers|notsuspended|suspended] [children|child]
|
||||
gam print orgs|ous [todrive] [toplevelonly] [from_parent <OrgUnitPath>] [allfields|(fields <OrgUnitFieldNameList>)]
|
||||
|
||||
gam create alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
|
||||
@@ -868,30 +937,76 @@ gam delete alias|nickname [user|group|target] <UniqueID>|<EmailAddress>
|
||||
gam info alias|nickname <EmailAddress>
|
||||
gam print aliases|nicknames [todrive] [shownoneditable] [nogroups] [nousers] [(query <QueryUser>)|(queries <QueryUserList)]
|
||||
|
||||
gam calendar <CalendarItem> add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> update <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default [sendnotifications <Boolean>]
|
||||
gam calendar <CalendarItem> update <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default [sendnotifications <Boolean>]
|
||||
gam calendar <CalendarItem> del|delete <CalendarACLRole> <EmailAddress>|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> del|delete id <CalendarACLRuleID>
|
||||
gam calendar <CalendarItem> showacl
|
||||
gam calendar <CalendarItem> printacl [todrive]
|
||||
|
||||
gam calendar <CalendarItem> addevent <EventAttributes>+
|
||||
gam calendar <CalendarItem> deleteevent (id|eventid <EventID>)* (query|eventquery <QueryCalendar>)* [doit] [notifyattendees]
|
||||
<EventNotificationAttribute> ::=
|
||||
notifyattendees|(sendnotifications <Boolean>)|(sendupdates all|enternalonly|none)
|
||||
|
||||
The following attributes are equivalent:
|
||||
notifyattendees - sendupdates all
|
||||
sendnotifications false - sendupdates none
|
||||
sendnotifications true - sendupdates all
|
||||
|
||||
<EventAttributes> ::=
|
||||
anyonecanaddself|
|
||||
(attendee <EmailAddress>)|
|
||||
available|
|
||||
(colorindex|colorid <EventColorIndex>)
|
||||
(description <String>)|
|
||||
(end (allday <Date>)|<Time>)|
|
||||
guestscantinviteothers|
|
||||
guestscantseeothers|
|
||||
(id <String>)|
|
||||
(location <String>)|
|
||||
(noreminders| (reminder <Number> email|popup|sms))|
|
||||
(optionalattendee <EmailAddress>)|
|
||||
(privateproperty <PropertyKey> <PropertyValue>)|
|
||||
(recurrence <RRULE, EXRULE, RDATE and EXDATE line>)|
|
||||
(sharedproperty <PropertyKey> <PropertyValue>)|
|
||||
(source <String> <URL>)|
|
||||
(start (allday <Date>)|<Time>)|
|
||||
(summary <String>)|
|
||||
tentative|
|
||||
(timezone <Timezone>)|
|
||||
(visibility default|public|prvate)
|
||||
|
||||
<EventSelectProperty:> ::=
|
||||
(after <Time>)|
|
||||
(before <Time>)|
|
||||
includeeleted|
|
||||
includehidden|
|
||||
(query <QueryCalendar>)|
|
||||
(updatedmin <Time>)
|
||||
|
||||
<EventDisplayProperty> ::=
|
||||
(timezone <TimeZone>)
|
||||
|
||||
gam calendar <CalendarItem> addevent <EventAttributes>+ [<EventNotificationAttribute>]
|
||||
gam calendar <CalendarItem> deleteevent id|eventid <EventID> [doit] [<EventNotificationAttribute>]
|
||||
gam calendar <CalendarItem> moveevent id|eventid <EventID> [doit] [<EventNotificationAttribute>]
|
||||
gam calendar <CalendarItem> wipe
|
||||
gam calendar <CalendarItem> printevents <EventSelectProperty>* <EventDisplayProperty>* [todrive]
|
||||
|
||||
<CalendarSettings> ::=
|
||||
summary <String>|
|
||||
description <String>|
|
||||
location <String>|
|
||||
timezone <String>
|
||||
timezone <TimeZone>
|
||||
|
||||
gam calendar <CalendarItem> modify <CalendarSettings>+
|
||||
|
||||
gam update cros <CrOSItem> (<CrOSAttributes>+)|(action deprovision_same_model_replace|deprovision_different_model_replace|deprovision_retiring_device|disable|reenable [acknowledge_device_touch_requirement])
|
||||
gam info cros <CrOSItem> [nolists] [listlimit <Number>] [start <Date>] [end <Date>]
|
||||
gam update cros <CrOSEntity> (<CrOSAttributes>+)|(action deprovision_same_model_replace|deprovision_different_model_replace|deprovision_retiring_device|disable|reenable [acknowledge_device_touch_requirement])
|
||||
gam info cros <CrOSEntity> [guessaue] [nolists] [listlimit <Number>] [start <Date>] [end <Date>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>] [downloadfile latest|<Time>] [targetfolder <FilePath>]
|
||||
|
||||
gam print cros [todrive] [(query <QueryCrOS>)|(queries <QueryCrOSList>)] [limittoou <OrgUnitItem>]
|
||||
[orderby <CrOSOrderByFieldName> [ascending|descending]] [nolists|recentusers|timeranges|devicefiles] [listlimit <Number>] [start <Date>] [end <Date>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
||||
[orderby <CrOSOrderByFieldName> [ascending|descending]] [guessaue] [nolists|<CrOSListFieldName>*] [listlimit <Number>] [start <Date>] [end <Date>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>] [sortheaders]
|
||||
gam <CrOSTypeEntity> print
|
||||
|
||||
Summary of printing:
|
||||
@@ -902,22 +1017,14 @@ Prints no header row and deviceId for specified CrOS devices.
|
||||
gam print cros ... basic|full
|
||||
Prints a header row and selected fields for specified CrOS devices.
|
||||
|
||||
The basic argument outputs these column headers: deviceId,annotatedAssetId,annotatedLocation,annotatedUser,lastSync,notes,serialNumber,status
|
||||
The allfields/full arguments output all column headers including three headers, recentUsers, activeTimeRanges and deviceFiles,
|
||||
that repeat with two/four/two subvalues each, yielding a large number of columns that make the output hard to process.
|
||||
The nolists argument suppresses these three headers; if you want these headers in a more manageable form use the following arguments.
|
||||
|
||||
If recentusers is specified, for each recent user, the columns recentUsers.email and recentUsers.type are output on a separate row
|
||||
with all of the other headers.
|
||||
|
||||
If timeranges is specified, for each time range entry, the columns activeTimeRanges.date, activeTimeRange.activeTime,
|
||||
activeTimeRanges.duration and activeTimeRanges.minutes are output on a separate row with all of the other headers.
|
||||
|
||||
If devicefiles is specified, for each deviceFile, the columns deviceFiles.type and deviceFiles.createTime are output on a separate row
|
||||
with all of the other headers.
|
||||
The basic argument outputs these column headers: deviceId,annotatedAssetId,annotatedLocation,annotatedUser,lastSync,notes,serialNumber,status.
|
||||
The allfields/full arguments output all column headers including six headers: activeTimeRanges, cpuStatusReports, deviceFiles, diskVolumeReports, recentUsers and systemRamFreeReports
|
||||
that repeat with multiple subvalues each, yielding a large number of columns that make the output hard to process.
|
||||
The nolists argument suppresses these six headers; if you want these headers in a more manageable form specify <CrOSListFieldName> values as desired.
|
||||
One set of values for all <CrOSListFieldName> fields specified will be output on a separate row with all of the other headers.
|
||||
|
||||
The listlimit <Number> argument limits the number of repetitions to <Number>; if not specified or <Number> equals zero, there is no limit.
|
||||
The start <Date> and end <Date> arguments filter the time ranges.
|
||||
The start <Date> and end <Date> arguments constrain activeTimeRanges, cpuStatusReports, deviceFiles and systemRamFreeReports to fall within the specified <Dates>.
|
||||
|
||||
gam print crosactivity [todrive] [(query <QueryCrOS>)|(queries <QueryCrOSList>)] [limittoou <OrgUnitItem>]
|
||||
[recentusers] [timeranges] [both] [devicefiles] [all] [listlimit <Number>] [start <Date>] [end <Date>] [delimiter <Character>]
|
||||
@@ -949,11 +1056,11 @@ gam print mobile [todrive] [(query <QueryMobile>)|(queries <QueryMobileList>)] [
|
||||
|
||||
gam create group <EmailAddress> <GroupAttributes>*
|
||||
gam update group <GroupItem> [admincreated <Boolean>] [email <EmailAddress>] <GroupAttributes>*
|
||||
gam update group <GroupItem> add [owner|manager|member] [notsuspended] <UserTypeEntity>
|
||||
gam update group <GroupItem> add [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
|
||||
gam update group <GroupItem> delete|remove [owner|manager|member] <UserTypeEntity>
|
||||
gam update group <GroupItem> sync [owner|manager|member] [notsuspended] <UserTypeEntity>
|
||||
gam update group <GroupItem> update [owner|manager|member] <UserTypeEntity>
|
||||
gam update group <GroupItem> clear [member] [manager] [owner] [suspended]
|
||||
gam update group <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
|
||||
gam update group <GroupItem> update [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
|
||||
gam update group <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
|
||||
gam delete group <GroupItem>
|
||||
gam info group <GroupItem> [nousers] [noaliases] [groups]
|
||||
|
||||
@@ -962,14 +1069,13 @@ gam print groups [todrive] ([domain <DomainName>] ([member <UserItem>]|[query <Q
|
||||
[members|memberscount] [managers|managerscount] [owners|ownerscount]
|
||||
[delimiter <Character>] [sortheaders]
|
||||
|
||||
gam print group-members|groups-members [todrive] ([domain <DomainName>] ([member <UserItem>]|[query <QueryGroup>]))|[group <GroupItem>]
|
||||
gam info member <UserItem> <GroupItem>
|
||||
gam print group-members|groups-members [todrive]
|
||||
([domain <DomainName>] ([member <UserItem>]|[query <QueryGroup>]))|[group|group_ns|group_susp <GroupItem>] [notsuspended|suspended]
|
||||
[roles <GroupRoleList>] [membernames] [fields <MembersFieldNameList>]
|
||||
|
||||
gam print license|licenses|licence|licences [todrive] [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
|
||||
gam update notification|notifications [(id all)|(id <NotificationID>)*] unread|read
|
||||
gam delete notification|notifications [(id all)|(id <NotificationID>)*]
|
||||
gam info notification|notifications [unreadonly]
|
||||
gam print licenses [todrive] [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite] [countsonly]
|
||||
gam show license|licenses|licence|licences [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite]
|
||||
|
||||
gam create building <Name> <BuildingAttributes>*
|
||||
gam update building <BuildIngID> <BuildingAttributes>*
|
||||
@@ -986,7 +1092,7 @@ gam create resource <ResourceID> <Name> <ResourceAttributes>*
|
||||
gam update resource <ResourceID> <ResourceAttributes>*
|
||||
gam delete resource <ResourceID>
|
||||
gam info resource <ResourceID>
|
||||
gam print resources [todrive] [allfields] <ResourceFieldName>*
|
||||
gam print resources [todrive] [allfields] <ResourceFieldName>* [query <String>]
|
||||
|
||||
gam create schema|schemas <SchemaName> <SchemaFieldDefinition>+
|
||||
gam update schema <SchemaName> <SchemaFieldDefinition>* (deletefield <FieldName>)*
|
||||
@@ -1001,18 +1107,17 @@ gam delete user <UserItem>
|
||||
gam undelete user <UserItem> [org|ou <OrgUnitPath>]
|
||||
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas|custom <SchemaNameList>] [userview] [skus|sku <SKUIDList>]
|
||||
|
||||
gam print users [todrive] ([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)] [deleted_only|only_deleted])
|
||||
Print fields for selected users; use domain, query/queries and deleted_only to select users to print;
|
||||
if none of these options are specified, all users are printed.
|
||||
The first column will always be primaryEmail; the remaining field names will be sorted if allfields, basic, full or sortheaders is specified;
|
||||
otherwise, the remaining field names will appear in the order specified.
|
||||
|
||||
gam print users [todrive]
|
||||
([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)] [deleted_only|only_deleted])
|
||||
[groups] [license|licenses|licence|licences] [emailpart|emailparts|username]
|
||||
[orderby <UserOrderByFieldName> [ascending|descending]] [userview]
|
||||
[allfields|basic|full | ((<UserFieldName>* | fields <UserFieldNameList>) [schemas|custom all|<SchemaNameList>])]
|
||||
[delimiter <Character>] [sortheaders]
|
||||
gam <UserTypeEntity> print
|
||||
|
||||
Summary of printing:
|
||||
gam print users
|
||||
Prints a header row and primaryEmail for all users.
|
||||
gam <UserTypeEntity> print
|
||||
Prints no header row and primaryEmail for specified users.
|
||||
|
||||
gam create verify|verification <DomainName>
|
||||
gam update verify|verification <DomainName> cname|txt|text|site|file
|
||||
@@ -1065,12 +1170,25 @@ gam print printjobs [todrive] [printer|printerid <PrinterID>]
|
||||
[owner|user <EmailAddress>]
|
||||
[limit <Number>]
|
||||
|
||||
gam create vaultexport|export matter <MatterItem> [name <name>] corpus <drive|mail|groups|hangouts_chat>
|
||||
(accounts <EmailAddressList>) | (orgunit|ou <OrgUnitPath>) | (teamdrives <TeamDriveList>) | (rooms <ChatRoomList>) | everyone
|
||||
[scope <all_data|held_data|unprocessed_data>]
|
||||
[terms <terms>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
|
||||
[excludedrafts <Boolean>] [format mbox|pst] [showconfidentialmodecontent <Boolean>]
|
||||
[includerooms]
|
||||
[driveversiondate <Date>|<Time>] [includeshareddrives|includeteamdrives] [includeaccessinfo <Boolean>]
|
||||
[region any|europe|us]
|
||||
gam delete export <MatterItem> <ExportItem>
|
||||
gam info export <MatterItem> <ExportItem>
|
||||
gam print exports [todrive] [matters <MatterItemList>]
|
||||
gam download export <MatterItem> <ExportItem> [noverify] [noextract] [targetfolder <FilePath>]
|
||||
|
||||
gam create vaulthold|hold corpus drive|groups|mail matter <MatterItem> [name <String>] [query <QueryVaultCorpus>]
|
||||
[(accounts|groups|users <EmailItemList>) | (orgunit|ou <OrgUnit>)]
|
||||
[starttime <Date>|<DateTime>] [endtime <Date>|<DateTime>]
|
||||
[start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||
gam update vaulthold|hold <HoldItem> matter <MatterItem> [query <QueryVaultCorpus>]
|
||||
[([addaccounts|addgroups|addusers <EmailItemList>] [removeaccounts|removegroups|removeusers <EmailItemList>]) | (orgunit|ou <OrgUnit>)]
|
||||
[starttime <Date>|<DateTime>] [endtime <Date>|<DateTime>]
|
||||
[start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||
gam delete vaulthold|hold <HoldItem> matter <MatterItem>
|
||||
gam info vaulthold|hold <HoldItem> matter <MatterItem>
|
||||
gam print vaultholds|holds [todrive] [matters <MatterItemList>]
|
||||
@@ -1103,7 +1221,7 @@ gam <UserTypeEntity> print calendars [todrive]
|
||||
|
||||
gam <UserTypeEntity> show calsettings
|
||||
gam <UserTypeEntity> update calattendees csv <FileName> [dryrun] [start <Date>] [end <Date>] [allevents]
|
||||
gam <UserTypeEntity> transfer seccals <UserItem> [keepuser]
|
||||
gam <UserTypeEntity> transfer seccals <UserItem> [keepuser] [sendnotifications <Boolean>]
|
||||
|
||||
gam <UserTypeEntity> print|show driveactivity [todrive] [fileid <DriveFileID>] [folderid <DriveFolderID>]
|
||||
gam <UserTypeEntity> print|show drivesettings [todrive]
|
||||
@@ -1116,8 +1234,9 @@ gam <UserTypeEntity> show filetree [anyowner] (orderby <DriveOrderByFieldName> [
|
||||
|
||||
gam <UserTypeEntity> create|add drivefile [drivefilename <DriveFileName>] <DriveFileAddAttributes>* [csv] [todrive]
|
||||
gam <UserTypeEntity> update drivefile (id <DriveFileID)|(drivefilename <DriveFileName>)|(query <QueryDriveFile) [copy] [newfilename <DriveFileName>] <DriveFileUpdateAttributes>*
|
||||
gam <UserTypeEntity> get drivefile (id <DriveFileID>)|(drivefilename <DriveFileName>)|(query <QueryDriveFile>) [revision <Number>] [format <FileFormatList>]
|
||||
targetfolder <FilePath>] [targetname <FileName>] [overwrite] [showprogress]
|
||||
gam <UserTypeEntity> get drivefile (id <DriveFileID>)|(drivefilename <DriveFileName>)|(query <QueryDriveFile>)
|
||||
[revision <Number>] [(format <FileFormatList>)|(csvsheet <String>)]
|
||||
[targetfolder <FilePath>] [targetname -|<FileName>] [overwrite] [showprogress]
|
||||
gam <UserTypeEntity> delete|del drivefile <DriveFileID>|<DriveFileURL>|(query:<QueryDriveFile>) [purge|untrash]
|
||||
gam <UserTypeEntity> transfer drive <UserItem> [keepuser]
|
||||
gam <UserTypeEntity> delete|del emptydrivefolders
|
||||
@@ -1168,7 +1287,21 @@ gam <UserTypeEntity> trash messages query <QueryGmail> [doit] [max_to_trash|max_
|
||||
gam <UserTypeEntity> untrash messages query <QueryGmail> [doit] [max_to_untrash|max_to_process <Number>]
|
||||
|
||||
gam <UserTypeEntity> show gmailprofile [todrive]
|
||||
gam <UserTypeEntity> show gplusprofile [todrive]
|
||||
|
||||
gam <UserTypeEntity> draftemail [recipient|to <EmailAddress>] [from <EmailAddress>]
|
||||
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
|
||||
gam <UserTypeEntity> importemail [recipient|to <EmailAddress>] [from <EmailAddress>]
|
||||
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
|
||||
[labels <LabelNameList>] (header <String> <String>)*
|
||||
[deleted] [date <Time>]
|
||||
[nevercheckspam] [processforcalendar]
|
||||
gam <UserTypeEntity> insertemail [recipient|to <EmailAddress>] [from <EmailAddress>]
|
||||
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
|
||||
[labels <LabelNameList>] (header <String> <String>)*
|
||||
[deleted] [date <Time>]
|
||||
gam <UserTypeEntity> sendemail [recipient|to <EmailAddress>] [from <EmailAddress>]
|
||||
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
|
||||
(header <String> <String>)*
|
||||
|
||||
gam <UserTypeEntity> create|add delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> delegate|delegates to <EmailAddress>
|
||||
@@ -1200,7 +1333,11 @@ gam <UserTypeEntity> show imap|imap4
|
||||
gam <UserTypeEntity> pop|pop3 <Boolean> [for allmail|newmail|mailfromnowon|fromnowown] [action keep|leaveininbox|archive|delete|trash|markread]
|
||||
gam <UserTypeEntity> show pop|pop3
|
||||
|
||||
gam <UserTypeEntity> language <Language>
|
||||
gam <UserTypeEntity> show language
|
||||
|
||||
gam <UserTypeEntity> [create|add] sendas <EmailAddress> <Name> [signature|sig <String>|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)*] [html] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
||||
[smtpmsa.host <SMTPHostName> smtpmsa.port 25|465|587 smtpmsa.username <UserName> smtpmsa.password <Password> [smtpmsa.securitymode none|ssl|starttls]]
|
||||
gam <UserTypeEntity> update sendas <EmailAddress> [name <Name>] [signature|sig <String>|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)*] [html] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
||||
gam <UserTypeEntity> delete sendas <EmailAddress>
|
||||
gam <UserTypeEntity> show sendas [format]
|
||||
@@ -1216,8 +1353,16 @@ gam <UserTypeEntity> print smime [todrive] [primaryonly]
|
||||
gam <UserTypeEntity> signature|sig <String>|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)* [html] [name <String>] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
||||
gam <UserTypeEntity> show signature|sig [format]
|
||||
|
||||
<TeamDriveRestrictionsSubfieldName> ::=
|
||||
adminmanagedrestrictions|
|
||||
copyrequireswriterpermission|
|
||||
domainusersonly|
|
||||
teammembersonly
|
||||
|
||||
gam <UserTypeEntity> create|add teamdrive <Name>
|
||||
gam <UserTypeEntity> update teamdrive <TeamDriveID> [name <Name>] [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
|
||||
gam <UserTypeEntity> update teamdrive <TeamDriveID> [asadmin] [name <Name>]
|
||||
[(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
|
||||
(<TeamDriveRestrictionsSubfieldName> <Boolean>)*
|
||||
gam <UserTypeEntity> delete teamdrive <TeamDriveID>
|
||||
gam <UserTypeEntity> show teamdriveinfo <TeamDriveID> [asadmin]
|
||||
gam <UserTypeEntity> show teamdrives [asadmin]
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
"""Extensible memoizing collections and decorators."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
|
||||
from . import keys
|
||||
from .cache import Cache
|
||||
from .lfu import LFUCache
|
||||
from .lru import LRUCache
|
||||
from .rr import RRCache
|
||||
from .ttl import TTLCache
|
||||
|
||||
__all__ = (
|
||||
'Cache', 'LFUCache', 'LRUCache', 'RRCache', 'TTLCache',
|
||||
'cached', 'cachedmethod'
|
||||
)
|
||||
|
||||
__version__ = '2.1.0'
|
||||
|
||||
if hasattr(functools.update_wrapper(lambda f: f(), lambda: 42), '__wrapped__'):
|
||||
_update_wrapper = functools.update_wrapper
|
||||
else:
|
||||
def _update_wrapper(wrapper, wrapped):
|
||||
functools.update_wrapper(wrapper, wrapped)
|
||||
wrapper.__wrapped__ = wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
def cached(cache, key=keys.hashkey, lock=None):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
results in a cache.
|
||||
|
||||
"""
|
||||
def decorator(func):
|
||||
if cache is None:
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
elif lock is None:
|
||||
def wrapper(*args, **kwargs):
|
||||
k = key(*args, **kwargs)
|
||||
try:
|
||||
return cache[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = func(*args, **kwargs)
|
||||
try:
|
||||
cache[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
else:
|
||||
def wrapper(*args, **kwargs):
|
||||
k = key(*args, **kwargs)
|
||||
try:
|
||||
with lock:
|
||||
return cache[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = func(*args, **kwargs)
|
||||
try:
|
||||
with lock:
|
||||
cache[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
return _update_wrapper(wrapper, func)
|
||||
return decorator
|
||||
|
||||
|
||||
def cachedmethod(cache, key=keys.hashkey, lock=None):
|
||||
"""Decorator to wrap a class or instance method with a memoizing
|
||||
callable that saves results in a cache.
|
||||
|
||||
"""
|
||||
def decorator(method):
|
||||
if lock is None:
|
||||
def wrapper(self, *args, **kwargs):
|
||||
c = cache(self)
|
||||
if c is None:
|
||||
return method(self, *args, **kwargs)
|
||||
k = key(self, *args, **kwargs)
|
||||
try:
|
||||
return c[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = method(self, *args, **kwargs)
|
||||
try:
|
||||
c[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
else:
|
||||
def wrapper(self, *args, **kwargs):
|
||||
c = cache(self)
|
||||
if c is None:
|
||||
return method(self, *args, **kwargs)
|
||||
k = key(self, *args, **kwargs)
|
||||
try:
|
||||
with lock(self):
|
||||
return c[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = method(self, *args, **kwargs)
|
||||
try:
|
||||
with lock(self):
|
||||
c[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
return _update_wrapper(wrapper, method)
|
||||
return decorator
|
||||
@@ -1,48 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class DefaultMapping(collections.MutableMapping):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def __contains__(self, key): # pragma: nocover
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, key): # pragma: nocover
|
||||
if hasattr(self.__class__, '__missing__'):
|
||||
return self.__class__.__missing__(self, key)
|
||||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
else:
|
||||
return default
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
if key in self:
|
||||
value = self[key]
|
||||
del self[key]
|
||||
elif default is self.__marker:
|
||||
raise KeyError(key)
|
||||
else:
|
||||
value = default
|
||||
return value
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key in self:
|
||||
value = self[key]
|
||||
else:
|
||||
self[key] = value = default
|
||||
return value
|
||||
|
||||
|
||||
DefaultMapping.register(dict)
|
||||
@@ -1,110 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from .abc import DefaultMapping
|
||||
|
||||
|
||||
class _DefaultSize(object):
|
||||
def __getitem__(self, _):
|
||||
return 1
|
||||
|
||||
def __setitem__(self, _, value):
|
||||
assert value == 1
|
||||
|
||||
def pop(self, _):
|
||||
return 1
|
||||
|
||||
|
||||
_deprecated = object()
|
||||
|
||||
|
||||
class Cache(DefaultMapping):
|
||||
"""Mutable mapping to serve as a simple cache or cache base class."""
|
||||
|
||||
__size = _DefaultSize()
|
||||
|
||||
def __init__(self, maxsize, missing=_deprecated, getsizeof=None):
|
||||
if missing is not _deprecated:
|
||||
warn("Cache constructor parameter 'missing' is deprecated",
|
||||
DeprecationWarning, 3)
|
||||
if missing:
|
||||
self.__missing = missing
|
||||
if getsizeof:
|
||||
self.getsizeof = getsizeof
|
||||
if self.getsizeof is not Cache.getsizeof:
|
||||
self.__size = dict()
|
||||
self.__data = dict()
|
||||
self.__currsize = 0
|
||||
self.__maxsize = maxsize
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, maxsize=%r, currsize=%r)' % (
|
||||
self.__class__.__name__,
|
||||
list(self.__data.items()),
|
||||
self.__maxsize,
|
||||
self.__currsize,
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self.__data[key]
|
||||
except KeyError:
|
||||
return self.__missing__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
maxsize = self.__maxsize
|
||||
size = self.getsizeof(value)
|
||||
if size > maxsize:
|
||||
raise ValueError('value too large')
|
||||
if key not in self.__data or self.__size[key] < size:
|
||||
while self.__currsize + size > maxsize:
|
||||
self.popitem()
|
||||
if key in self.__data:
|
||||
diffsize = size - self.__size[key]
|
||||
else:
|
||||
diffsize = size
|
||||
self.__data[key] = value
|
||||
self.__size[key] = size
|
||||
self.__currsize += diffsize
|
||||
|
||||
def __delitem__(self, key):
|
||||
size = self.__size.pop(key)
|
||||
del self.__data[key]
|
||||
self.__currsize -= size
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__data
|
||||
|
||||
def __missing__(self, key):
|
||||
value = self.__missing(key)
|
||||
try:
|
||||
self.__setitem__(key, value)
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return value
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__data)
|
||||
|
||||
@property
|
||||
def maxsize(self):
|
||||
"""The maximum size of the cache."""
|
||||
return self.__maxsize
|
||||
|
||||
@property
|
||||
def currsize(self):
|
||||
"""The current size of the cache."""
|
||||
return self.__currsize
|
||||
|
||||
@staticmethod
|
||||
def getsizeof(value):
|
||||
"""Return the size of a cache element's value."""
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def __missing(key):
|
||||
raise KeyError(key)
|
||||
@@ -1,106 +0,0 @@
|
||||
"""`functools.lru_cache` compatible memoizing function decorators."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import random
|
||||
import time
|
||||
|
||||
try:
|
||||
from threading import RLock
|
||||
except ImportError:
|
||||
from dummy_threading import RLock
|
||||
|
||||
from . import keys
|
||||
from .lfu import LFUCache
|
||||
from .lru import LRUCache
|
||||
from .rr import RRCache
|
||||
from .ttl import TTLCache
|
||||
|
||||
__all__ = ('lfu_cache', 'lru_cache', 'rr_cache', 'ttl_cache')
|
||||
|
||||
|
||||
_CacheInfo = collections.namedtuple('CacheInfo', [
|
||||
'hits', 'misses', 'maxsize', 'currsize'
|
||||
])
|
||||
|
||||
|
||||
def _cache(cache, typed=False):
|
||||
def decorator(func):
|
||||
key = keys.typedkey if typed else keys.hashkey
|
||||
lock = RLock()
|
||||
stats = [0, 0]
|
||||
|
||||
def cache_info():
|
||||
with lock:
|
||||
hits, misses = stats
|
||||
maxsize = cache.maxsize
|
||||
currsize = cache.currsize
|
||||
return _CacheInfo(hits, misses, maxsize, currsize)
|
||||
|
||||
def cache_clear():
|
||||
with lock:
|
||||
try:
|
||||
cache.clear()
|
||||
finally:
|
||||
stats[:] = [0, 0]
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
k = key(*args, **kwargs)
|
||||
with lock:
|
||||
try:
|
||||
v = cache[k]
|
||||
stats[0] += 1
|
||||
return v
|
||||
except KeyError:
|
||||
stats[1] += 1
|
||||
v = func(*args, **kwargs)
|
||||
try:
|
||||
with lock:
|
||||
cache[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
functools.update_wrapper(wrapper, func)
|
||||
if not hasattr(wrapper, '__wrapped__'):
|
||||
wrapper.__wrapped__ = func # Python 2.7
|
||||
wrapper.cache_info = cache_info
|
||||
wrapper.cache_clear = cache_clear
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def lfu_cache(maxsize=128, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Least Frequently Used (LFU)
|
||||
algorithm.
|
||||
|
||||
"""
|
||||
return _cache(LFUCache(maxsize), typed)
|
||||
|
||||
|
||||
def lru_cache(maxsize=128, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Least Recently Used (LRU)
|
||||
algorithm.
|
||||
|
||||
"""
|
||||
return _cache(LRUCache(maxsize), typed)
|
||||
|
||||
|
||||
def rr_cache(maxsize=128, choice=random.choice, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Random Replacement (RR)
|
||||
algorithm.
|
||||
|
||||
"""
|
||||
return _cache(RRCache(maxsize, choice), typed)
|
||||
|
||||
|
||||
def ttl_cache(maxsize=128, ttl=600, timer=time.time, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Least Recently Used (LRU)
|
||||
algorithm with a per-item time-to-live (TTL) value.
|
||||
"""
|
||||
return _cache(TTLCache(maxsize, ttl, timer), typed)
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Key functions for memoizing decorators."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__all__ = ('hashkey', 'typedkey')
|
||||
|
||||
|
||||
class _HashedTuple(tuple):
|
||||
|
||||
__hashvalue = None
|
||||
|
||||
def __hash__(self, hash=tuple.__hash__):
|
||||
hashvalue = self.__hashvalue
|
||||
if hashvalue is None:
|
||||
self.__hashvalue = hashvalue = hash(self)
|
||||
return hashvalue
|
||||
|
||||
def __add__(self, other, add=tuple.__add__):
|
||||
return _HashedTuple(add(self, other))
|
||||
|
||||
def __radd__(self, other, add=tuple.__add__):
|
||||
return _HashedTuple(add(other, self))
|
||||
|
||||
|
||||
_kwmark = (object(),)
|
||||
|
||||
|
||||
def hashkey(*args, **kwargs):
|
||||
"""Return a cache key for the specified hashable arguments."""
|
||||
|
||||
if kwargs:
|
||||
return _HashedTuple(args + sum(sorted(kwargs.items()), _kwmark))
|
||||
else:
|
||||
return _HashedTuple(args)
|
||||
|
||||
|
||||
def typedkey(*args, **kwargs):
|
||||
"""Return a typed cache key for the specified hashable arguments."""
|
||||
|
||||
key = hashkey(*args, **kwargs)
|
||||
key += tuple(type(v) for v in args)
|
||||
key += tuple(type(v) for _, v in sorted(kwargs.items()))
|
||||
return key
|
||||
@@ -1,35 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
from .cache import Cache, _deprecated
|
||||
|
||||
|
||||
class LFUCache(Cache):
|
||||
"""Least Frequently Used (LFU) cache implementation."""
|
||||
|
||||
def __init__(self, maxsize, missing=_deprecated, getsizeof=None):
|
||||
Cache.__init__(self, maxsize, missing, getsizeof)
|
||||
self.__counter = collections.Counter()
|
||||
|
||||
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
|
||||
value = cache_getitem(self, key)
|
||||
self.__counter[key] -= 1
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
||||
cache_setitem(self, key, value)
|
||||
self.__counter[key] -= 1
|
||||
|
||||
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
||||
cache_delitem(self, key)
|
||||
del self.__counter[key]
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return the `(key, value)` pair least frequently used."""
|
||||
try:
|
||||
(key, _), = self.__counter.most_common(1)
|
||||
except ValueError:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
@@ -1,48 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
from .cache import Cache, _deprecated
|
||||
|
||||
|
||||
class LRUCache(Cache):
|
||||
"""Least Recently Used (LRU) cache implementation."""
|
||||
|
||||
def __init__(self, maxsize, missing=_deprecated, getsizeof=None):
|
||||
Cache.__init__(self, maxsize, missing, getsizeof)
|
||||
self.__order = collections.OrderedDict()
|
||||
|
||||
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
|
||||
value = cache_getitem(self, key)
|
||||
self.__update(key)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
||||
cache_setitem(self, key, value)
|
||||
self.__update(key)
|
||||
|
||||
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
||||
cache_delitem(self, key)
|
||||
del self.__order[key]
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return the `(key, value)` pair least recently used."""
|
||||
try:
|
||||
key = next(iter(self.__order))
|
||||
except StopIteration:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
|
||||
if hasattr(collections.OrderedDict, 'move_to_end'):
|
||||
def __update(self, key):
|
||||
try:
|
||||
self.__order.move_to_end(key)
|
||||
except KeyError:
|
||||
self.__order[key] = None
|
||||
else:
|
||||
def __update(self, key):
|
||||
try:
|
||||
self.__order[key] = self.__order.pop(key)
|
||||
except KeyError:
|
||||
self.__order[key] = None
|
||||
@@ -1,37 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import random
|
||||
|
||||
from .cache import Cache, _deprecated
|
||||
|
||||
|
||||
# random.choice cannot be pickled in Python 2.7
|
||||
def _choice(seq):
|
||||
return random.choice(seq)
|
||||
|
||||
|
||||
class RRCache(Cache):
|
||||
"""Random Replacement (RR) cache implementation."""
|
||||
|
||||
def __init__(self, maxsize, choice=random.choice, missing=_deprecated,
|
||||
getsizeof=None):
|
||||
Cache.__init__(self, maxsize, missing, getsizeof)
|
||||
# TODO: use None as default, assing to self.choice directly?
|
||||
if choice is random.choice:
|
||||
self.__choice = _choice
|
||||
else:
|
||||
self.__choice = choice
|
||||
|
||||
@property
|
||||
def choice(self):
|
||||
"""The `choice` function used by the cache."""
|
||||
return self.__choice
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return a random `(key, value)` pair."""
|
||||
try:
|
||||
key = self.__choice(list(self))
|
||||
except IndexError:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
@@ -1,217 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import time
|
||||
|
||||
from .cache import Cache, _deprecated
|
||||
|
||||
|
||||
class _Link(object):
|
||||
|
||||
__slots__ = ('key', 'expire', 'next', 'prev')
|
||||
|
||||
def __init__(self, key=None, expire=None):
|
||||
self.key = key
|
||||
self.expire = expire
|
||||
|
||||
def __reduce__(self):
|
||||
return _Link, (self.key, self.expire)
|
||||
|
||||
def unlink(self):
|
||||
next = self.next
|
||||
prev = self.prev
|
||||
prev.next = next
|
||||
next.prev = prev
|
||||
|
||||
|
||||
class _Timer(object):
|
||||
|
||||
def __init__(self, timer):
|
||||
self.__timer = timer
|
||||
self.__nesting = 0
|
||||
|
||||
def __call__(self):
|
||||
if self.__nesting == 0:
|
||||
return self.__timer()
|
||||
else:
|
||||
return self.__time
|
||||
|
||||
def __enter__(self):
|
||||
if self.__nesting == 0:
|
||||
self.__time = time = self.__timer()
|
||||
else:
|
||||
time = self.__time
|
||||
self.__nesting += 1
|
||||
return time
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.__nesting -= 1
|
||||
|
||||
def __reduce__(self):
|
||||
return _Timer, (self.__timer,)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.__timer, name)
|
||||
|
||||
|
||||
class TTLCache(Cache):
|
||||
"""LRU Cache implementation with per-item time-to-live (TTL) value."""
|
||||
|
||||
def __init__(self, maxsize, ttl, timer=time.time, missing=_deprecated,
|
||||
getsizeof=None):
|
||||
Cache.__init__(self, maxsize, missing, getsizeof)
|
||||
self.__root = root = _Link()
|
||||
root.prev = root.next = root
|
||||
self.__links = collections.OrderedDict()
|
||||
self.__timer = _Timer(timer)
|
||||
self.__ttl = ttl
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
link = self.__links[key] # no reordering
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
return not (link.expire < self.__timer())
|
||||
|
||||
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
|
||||
try:
|
||||
link = self.__getlink(key)
|
||||
except KeyError:
|
||||
expired = False
|
||||
else:
|
||||
expired = link.expire < self.__timer()
|
||||
if expired:
|
||||
return self.__missing__(key)
|
||||
else:
|
||||
return cache_getitem(self, key)
|
||||
|
||||
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
cache_setitem(self, key, value)
|
||||
try:
|
||||
link = self.__getlink(key)
|
||||
except KeyError:
|
||||
self.__links[key] = link = _Link(key)
|
||||
else:
|
||||
link.unlink()
|
||||
link.expire = time + self.__ttl
|
||||
link.next = root = self.__root
|
||||
link.prev = prev = root.prev
|
||||
prev.next = root.prev = link
|
||||
|
||||
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
||||
cache_delitem(self, key)
|
||||
link = self.__links.pop(key)
|
||||
link.unlink()
|
||||
if link.expire < self.__timer():
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self):
|
||||
root = self.__root
|
||||
curr = root.next
|
||||
while curr is not root:
|
||||
# "freeze" time for iterator access
|
||||
with self.__timer as time:
|
||||
if not (curr.expire < time):
|
||||
yield curr.key
|
||||
curr = curr.next
|
||||
|
||||
def __len__(self):
|
||||
root = self.__root
|
||||
curr = root.next
|
||||
time = self.__timer()
|
||||
count = len(self.__links)
|
||||
while curr is not root and curr.expire < time:
|
||||
count -= 1
|
||||
curr = curr.next
|
||||
return count
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
root = self.__root
|
||||
root.prev = root.next = root
|
||||
for link in sorted(self.__links.values(), key=lambda obj: obj.expire):
|
||||
link.next = root
|
||||
link.prev = prev = root.prev
|
||||
prev.next = root.prev = link
|
||||
self.expire(self.__timer())
|
||||
|
||||
def __repr__(self, cache_repr=Cache.__repr__):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
return cache_repr(self)
|
||||
|
||||
@property
|
||||
def currsize(self):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
return super(TTLCache, self).currsize
|
||||
|
||||
@property
|
||||
def timer(self):
|
||||
"""The timer function used by the cache."""
|
||||
return self.__timer
|
||||
|
||||
@property
|
||||
def ttl(self):
|
||||
"""The time-to-live value of the cache's items."""
|
||||
return self.__ttl
|
||||
|
||||
def expire(self, time=None):
|
||||
"""Remove expired items from the cache."""
|
||||
if time is None:
|
||||
time = self.__timer()
|
||||
root = self.__root
|
||||
curr = root.next
|
||||
links = self.__links
|
||||
cache_delitem = Cache.__delitem__
|
||||
while curr is not root and curr.expire < time:
|
||||
cache_delitem(self, curr.key)
|
||||
del links[curr.key]
|
||||
next = curr.next
|
||||
curr.unlink()
|
||||
curr = next
|
||||
|
||||
def clear(self):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
Cache.clear(self)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
with self.__timer:
|
||||
return Cache.get(self, *args, **kwargs)
|
||||
|
||||
def pop(self, *args, **kwargs):
|
||||
with self.__timer:
|
||||
return Cache.pop(self, *args, **kwargs)
|
||||
|
||||
def setdefault(self, *args, **kwargs):
|
||||
with self.__timer:
|
||||
return Cache.setdefault(self, *args, **kwargs)
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return the `(key, value)` pair least recently used that
|
||||
has not already expired.
|
||||
|
||||
"""
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
try:
|
||||
key = next(iter(self.__links))
|
||||
except StopIteration:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
|
||||
if hasattr(collections.OrderedDict, 'move_to_end'):
|
||||
def __getlink(self, key):
|
||||
value = self.__links[key]
|
||||
self.__links.move_to_end(key)
|
||||
return value
|
||||
else:
|
||||
def __getlink(self, key):
|
||||
value = self.__links.pop(key)
|
||||
self.__links[key] = value
|
||||
return value
|
||||
256
src/cros-aue-dates.json
Normal file
256
src/cros-aue-dates.json
Normal file
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"acer ac700": "2016-08-01T00:00:00.000Z",
|
||||
"acer c7 chromebook": "2017-10-01T00:00:00.000Z",
|
||||
"acer c7 chromebook (c710)": "2017-10-01T00:00:00.000Z",
|
||||
"acer c720 chromebook": "2019-06-01T00:00:00.000Z",
|
||||
"acer c740 chromebook": "2019-06-01T00:00:00.000Z",
|
||||
"acer chromebase": "2020-08-01T00:00:00.000Z",
|
||||
"acer chromebase 24": "2021-06-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (c720, c720p)": "2019-06-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (c732, c732t, c732l, c732lt)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (c740)": "2020-06-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (c771, c771t)": "2022-11-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (cb3-111, c730, c730e)": "2019-08-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (cb3-131, c735)": "2021-01-01T00:00:00.000Z",
|
||||
"acer chromebook 11 (cb311-8h, cb311-8ht)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook 11 n7 (c731, c731t)": "2022-01-01T00:00:00.000Z",
|
||||
"acer chromebook 13 (cb5-311)": "2019-09-01T00:00:00.000Z",
|
||||
"acer chromebook 13 (cb713-1w)": "2024-06-01T00:00:00.000Z",
|
||||
"acer chromebook 13(cb5-311, c810)": "2019-09-01T00:00:00.000Z",
|
||||
"acer chromebook 14 (cb3-431)": "2021-06-01T00:00:00.000Z",
|
||||
"acer chromebook 14 for work (cp5-471)": "2022-11-01T00:00:00.000Z",
|
||||
"acer chromebook 15 (c910 / cb5-571)": "2020-06-01T00:00:00.000Z",
|
||||
"acer chromebook 15 (cb3-531)": "2020-06-01T00:00:00.000Z",
|
||||
"acer chromebook 15 (cb3-532)": "2021-08-01T00:00:00.000Z",
|
||||
"acer chromebook 15 (cb315-1h,cb315-1ht)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook 15 (cb5-571, c910)": "2020-06-01T00:00:00.000Z",
|
||||
"acer chromebook 15 (cb515-1h,cb515-1ht)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook 311": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook 311 (c721, c733, c733u, c733t)": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook 315": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook 315 (cb315-2h)": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook 512 (c851, c851t)": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook 514": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook 714 (cb714-1w / cb714-1wt)": "2024-06-01T00:00:00.000Z",
|
||||
"acer chromebook 715 (cb715-1w / cb715-1wt)": "2024-06-01T00:00:00.000Z",
|
||||
"acer chromebook r11 (cb5-132t, c738t)": "2021-06-01T00:00:00.000Z",
|
||||
"acer chromebook r13 (cb5-312t)": "2021-09-01T00:00:00.000Z",
|
||||
"acer chromebook spin 11 (cp311-h1, cp311-1hn)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook spin 11 (r751t)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook spin 13 (cp713-1wn)": "2024-06-01T00:00:00.000Z",
|
||||
"acer chromebook spin 15 (cp315)": "2023-11-01T00:00:00.000Z",
|
||||
"acer chromebook spin 311 (r721t)": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook spin 511": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook spin 511 (r752t, r752tn)": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook spin 512 (r851tn)": "2025-06-01T00:00:00.000Z",
|
||||
"acer chromebook tab 10": "2023-08-01T00:00:00.000Z",
|
||||
"acer chromebox": "2019-09-01T00:00:00.000Z",
|
||||
"acer chromebox cxi2": "2020-06-01T00:00:00.000Z",
|
||||
"acer chromebox cxi2 / cxv2": "2020-06-01T00:00:00.000Z",
|
||||
"acer chromebox cxi3": "2024-06-01T00:00:00.000Z",
|
||||
"aopen chromebase commercial": "2020-09-01T00:00:00.000Z",
|
||||
"aopen chromebase mini": "2022-02-01T00:00:00.000Z",
|
||||
"aopen chromebox commercial": "2020-09-01T00:00:00.000Z",
|
||||
"aopen chromebox commercial 2": "2024-06-01T00:00:00.000Z",
|
||||
"aopen chromebox mini": "2022-02-01T00:00:00.000Z",
|
||||
"asi chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"asus chromebit cs10": "2020-11-01T00:00:00.000Z",
|
||||
"asus chromebook c200": "2019-06-01T00:00:00.000Z",
|
||||
"asus chromebook c200ma": "2019-06-01T00:00:00.000Z",
|
||||
"asus chromebook c201pa": "2020-06-01T00:00:00.000Z",
|
||||
"asus chromebook c202sa": "2021-06-01T00:00:00.000Z",
|
||||
"asus chromebook c204": "2025-06-01T00:00:00.000Z",
|
||||
"asus chromebook c213na": "2023-11-01T00:00:00.000Z",
|
||||
"asus chromebook c223": "2023-11-01T00:00:00.000Z",
|
||||
"asus chromebook c300": "2019-08-01T00:00:00.000Z",
|
||||
"asus chromebook c300ma": "2019-08-01T00:00:00.000Z",
|
||||
"asus chromebook c300sa / c301sa": "2021-06-01T00:00:00.000Z",
|
||||
"asus chromebook c403": "2023-11-01T00:00:00.000Z",
|
||||
"asus chromebook c423": "2023-11-01T00:00:00.000Z",
|
||||
"asus chromebook c523": "2023-11-01T00:00:00.000Z",
|
||||
"asus chromebook flip c100pa": "2020-07-01T00:00:00.000Z",
|
||||
"asus chromebook flip c101pa": "2023-08-01T00:00:00.000Z",
|
||||
"asus chromebook flip c213": "2023-11-01T00:00:00.000Z",
|
||||
"asus chromebook flip c214": "2025-06-01T00:00:00.000Z",
|
||||
"asus chromebook flip c302": "2022-11-01T00:00:00.000Z",
|
||||
"asus chromebook flip c434": "2024-06-01T00:00:00.000Z",
|
||||
"asus chromebook tablet ct100": "2023-08-01T00:00:00.000Z",
|
||||
"asus chromebox (cn60)": "2019-09-01T00:00:00.000Z",
|
||||
"asus chromebox 2 (cn62)": "2021-06-01T00:00:00.000Z",
|
||||
"asus chromebox 3": "2024-06-01T00:00:00.000Z",
|
||||
"asus chromebox 3 (cn65)": "2024-06-01T00:00:00.000Z",
|
||||
"asus chromebox cn60": "2019-09-01T00:00:00.000Z",
|
||||
"asus chromebox cn62": "2021-06-01T00:00:00.000Z",
|
||||
"bobicus chromebook 11": "2020-06-01T00:00:00.000Z",
|
||||
"chromebook 11 (c730 / cb3-111)": "2019-08-01T00:00:00.000Z",
|
||||
"chromebook 11 (c735)": "2021-01-01T00:00:00.000Z",
|
||||
"chromebook 15 (cb515 - 1ht / 1h)": "2023-11-01T00:00:00.000Z",
|
||||
"chromebook 311 (c721)": "2025-06-01T00:00:00.000Z",
|
||||
"chromebook pcm-116e": "2020-06-01T00:00:00.000Z",
|
||||
"consumer chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"cr-48": "2015-12-01T00:00:00.000Z",
|
||||
"crambo chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"ctl chromebook j41 / j41t": "2023-11-01T00:00:00.000Z",
|
||||
"ctl chromebook nl7": "2023-11-01T00:00:00.000Z",
|
||||
"ctl chromebook nl7t-360 / nl7tw-360": "2023-11-01T00:00:00.000Z",
|
||||
"ctl chromebook tab tx1": "2023-08-01T00:00:00.000Z",
|
||||
"ctl chromebook tablet tx1 for education": "2023-08-01T00:00:00.000Z",
|
||||
"ctl chromebox cbx1": "2024-06-01T00:00:00.000Z",
|
||||
"ctl j2 / j4 chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"ctl j5 chromebook": "2021-08-01T00:00:00.000Z",
|
||||
"ctl n6 education chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"ctl nl61 chromebook": "2021-08-01T00:00:00.000Z",
|
||||
"dell chromebook 11": "2019-06-01T00:00:00.000Z",
|
||||
"dell chromebook 11 (3120)": "2020-06-01T00:00:00.000Z",
|
||||
"dell chromebook 11 (3180)": "2022-05-01T00:00:00.000Z",
|
||||
"dell chromebook 11 (5190)": "2023-11-01T00:00:00.000Z",
|
||||
"dell chromebook 11 2-in-1 (3189)": "2022-05-01T00:00:00.000Z",
|
||||
"dell chromebook 11 2-in-1 (5190)": "2023-11-01T00:00:00.000Z",
|
||||
"dell chromebook 13 (3380)": "2022-11-01T00:00:00.000Z",
|
||||
"dell chromebook 13 (7310)": "2020-09-01T00:00:00.000Z",
|
||||
"dell chromebook 3100": "2025-06-01T00:00:00.000Z",
|
||||
"dell chromebook 3100 2-in-1": "2025-06-01T00:00:00.000Z",
|
||||
"dell chromebook 3400": "2025-06-01T00:00:00.000Z",
|
||||
"dell chromebox": "2019-09-01T00:00:00.000Z",
|
||||
"dell inspiron chromebook 14 2-in-1 (7486)": "2024-06-01T00:00:00.000Z",
|
||||
"edugear chromebook k": "2020-06-01T00:00:00.000Z",
|
||||
"edugear chromebook m": "2020-06-01T00:00:00.000Z",
|
||||
"edugear chromebook r": "2020-06-01T00:00:00.000Z",
|
||||
"edugear cmt chromebook": "2021-08-01T00:00:00.000Z",
|
||||
"edxis chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"edxis education chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"epik 11.6\" chromebook elb1101": "2020-06-01T00:00:00.000Z",
|
||||
"google chromebook pixel": "2018-06-01T00:00:00.000Z",
|
||||
"google chromebook pixel (2015)": "2020-06-01T00:00:00.000Z",
|
||||
"google cr-48": "2015-12-01T00:00:00.000Z",
|
||||
"google pixel slate": "2024-06-01T00:00:00.000Z",
|
||||
"google pixelbook": "2024-06-01T00:00:00.000Z",
|
||||
"haier chromebook 11": "2020-06-01T00:00:00.000Z",
|
||||
"haier chromebook 11 c": "2021-08-01T00:00:00.000Z",
|
||||
"haier chromebook 11 g2": "2020-09-01T00:00:00.000Z",
|
||||
"haier chromebook 11e": "2020-06-01T00:00:00.000Z",
|
||||
"hexa chromebook pi": "2020-06-01T00:00:00.000Z",
|
||||
"hisense chromebook 11": "2020-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 1100-1199 / hp chromebook 11 g1": "2018-10-01T00:00:00.000Z",
|
||||
"hp chromebook 11 2000-2099 / hp chromebook 11 g2": "2019-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 2100-2199 / hp chromebook 11 g3": "2020-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 2200-2299 / hp chromebook 11 g4/g4 ee": "2020-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g1": "2018-10-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g2": "2019-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g3": "2020-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g4/g4 ee": "2020-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g5": "2021-07-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g5 / hp chromebook 11-vxxx": "2021-07-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g5 ee": "2022-01-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g6 ee": "2023-11-01T00:00:00.000Z",
|
||||
"hp chromebook 11 g7 ee": "2025-06-01T00:00:00.000Z",
|
||||
"hp chromebook 11a g6 ee": "2025-06-01T00:00:00.000Z",
|
||||
"hp chromebook 13 g1": "2022-11-01T00:00:00.000Z",
|
||||
"hp chromebook 14": "2019-06-01T00:00:00.000Z",
|
||||
"hp chromebook 14 / hp chromebook 14 g5": "2023-11-01T00:00:00.000Z",
|
||||
"hp chromebook 14 ak000-099 / hp chromebook 14 g4": "2021-09-01T00:00:00.000Z",
|
||||
"hp chromebook 14 db0000-db0999": "2025-06-01T00:00:00.000Z",
|
||||
"hp chromebook 14 g3": "2019-10-01T00:00:00.000Z",
|
||||
"hp chromebook 14 g4": "2021-09-01T00:00:00.000Z",
|
||||
"hp chromebook 14 g5": "2023-11-01T00:00:00.000Z",
|
||||
"hp chromebook 14 x000-x999 / hp chromebook 14 g3": "2019-10-01T00:00:00.000Z",
|
||||
"hp chromebook 14a g5": "2025-06-01T00:00:00.000Z",
|
||||
"hp chromebook 15 g1": "2024-06-01T00:00:00.000Z",
|
||||
"hp chromebook x2 ": "2024-06-01T00:00:00.000Z",
|
||||
"hp chromebook x360 11 g1 ee": "2023-11-01T00:00:00.000Z",
|
||||
"hp chromebook x360 11 g2 ee": "2025-06-01T00:00:00.000Z",
|
||||
"hp chromebook x360 14": "2024-06-01T00:00:00.000Z",
|
||||
"hp chromebook x360 14 g1": "2024-06-01T00:00:00.000Z",
|
||||
"hp chromebox cb1-(000-099) / hp chromebox g1/ hp chromebox for meetings": "2019-09-01T00:00:00.000Z",
|
||||
"hp chromebox g1": "2019-09-01T00:00:00.000Z",
|
||||
"hp chromebox g2": "2024-06-01T00:00:00.000Z",
|
||||
"hp pavilion chromebook 14": "2018-02-01T00:00:00.000Z",
|
||||
"jp sa couto chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"lava xolo chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"lenovo 100e chromebook": "2023-11-01T00:00:00.000Z",
|
||||
"lenovo 100e chromebook 2nd gen": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo 100e chromebook 2nd gen mtk": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo 100s chromebook": "2020-09-01T00:00:00.000Z",
|
||||
"lenovo 14e chromebook": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo 300e chromebook": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo 300e chromebook 2nd gen": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo 300e chromebook 2nd gen mtk": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo 500e chromebook": "2023-11-01T00:00:00.000Z",
|
||||
"lenovo 500e chromebook 2nd gen": "2025-06-01T00:00:00.000Z",
|
||||
"lenovo chromebook c330": "2022-06-01T00:00:00.000Z",
|
||||
"lenovo chromebook s330": "2022-06-01T00:00:00.000Z",
|
||||
"lenovo flex 11 chromebook": "2022-06-01T00:00:00.000Z",
|
||||
"lenovo ideapad c330 chromebook": "2022-06-01T00:00:00.000Z",
|
||||
"lenovo ideapad s330 chromebook": "2022-06-01T00:00:00.000Z",
|
||||
"lenovo n20 chromebook": "2019-06-01T00:00:00.000Z",
|
||||
"lenovo n21 chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"lenovo n22 chromebook": "2021-06-01T00:00:00.000Z",
|
||||
"lenovo n23 chromebook": "2021-06-01T00:00:00.000Z",
|
||||
"lenovo n23 yoga chromebook": "2022-06-01T00:00:00.000Z",
|
||||
"lenovo n42 chromebook": "2021-06-01T00:00:00.000Z",
|
||||
"lenovo thinkcentre chromebox": "2020-06-01T00:00:00.000Z",
|
||||
"lenovo thinkpad 11e 3rd gen chromebook": "2021-06-01T00:00:00.000Z",
|
||||
"lenovo thinkpad 11e 4th gen chromebook": "2023-11-01T00:00:00.000Z",
|
||||
"lenovo thinkpad 11e chromebook": "2019-06-01T00:00:00.000Z",
|
||||
"lenovo thinkpad 11e chromebook (4th gen)/lenovo thinkpad yoga 11e chromebook (4th gen)": "2023-11-01T00:00:00.000Z",
|
||||
"lenovo thinkpad 13": "2022-11-01T00:00:00.000Z",
|
||||
"lenovo thinkpad x131e chromebook": "2018-06-01T00:00:00.000Z",
|
||||
"lenovo yoga c630 chromebook": "2024-06-01T00:00:00.000Z",
|
||||
"lg chromebase (22cb25s)": "2020-06-01T00:00:00.000Z",
|
||||
"lg chromebase (22cv241)": "2019-06-01T00:00:00.000Z",
|
||||
"lumos education chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"m&a chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"mecer chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"mecer v2 chromebook": "2021-08-01T00:00:00.000Z",
|
||||
"medion chromebook akoya s2013 ": "2020-06-01T00:00:00.000Z",
|
||||
"medion chromebook s2015": "2020-06-01T00:00:00.000Z",
|
||||
"multilaser chromebook m11c": "2021-08-01T00:00:00.000Z",
|
||||
"ncomputing chromebook cx100": "2020-06-01T00:00:00.000Z",
|
||||
"ncomputing chromebook cx110": "2020-06-01T00:00:00.000Z",
|
||||
"nexian chromebook 11.6\"": "2020-06-01T00:00:00.000Z",
|
||||
"pcmerge chromebook al116": "2023-11-01T00:00:00.000Z",
|
||||
"pcmerge chromebookpcm-116e/pcm-116eb": "2020-06-01T00:00:00.000Z",
|
||||
"pcmerge chromebookpcm-116t-432b": "2021-08-01T00:00:00.000Z",
|
||||
"poin2 chromebook 11": "2020-06-01T00:00:00.000Z",
|
||||
"poin2 chromebook 11c": "2022-11-01T00:00:00.000Z",
|
||||
"poin2 chromebook 14": "2022-03-01T00:00:00.000Z",
|
||||
"positivo chromebook c216b": "2021-08-01T00:00:00.000Z",
|
||||
"positivo chromebook ch1190": "2020-06-01T00:00:00.000Z",
|
||||
"promethean chromebox": "2024-06-01T00:00:00.000Z",
|
||||
"prowise 11.6\" entry line chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"prowise chromebook eduline": "2023-11-01T00:00:00.000Z",
|
||||
"prowise chromebook entryline": "2020-06-01T00:00:00.000Z",
|
||||
"prowise chromebook proline": "2021-08-01T00:00:00.000Z",
|
||||
"prowise proline chromebook": "2021-08-01T00:00:00.000Z",
|
||||
"rgs education chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"samsung chromebook": "2018-07-01T00:00:00.000Z",
|
||||
"samsung chromebook - xe303": "2018-07-01T00:00:00.000Z",
|
||||
"samsung chromebook 2 11": "2019-06-01T00:00:00.000Z",
|
||||
"samsung chromebook 2 11 - xe500c12": "2020-06-01T00:00:00.000Z",
|
||||
"samsung chromebook 2 13": "2019-06-01T00:00:00.000Z",
|
||||
"samsung chromebook 3": "2021-06-01T00:00:00.000Z",
|
||||
"samsung chromebook plus": "2023-08-01T00:00:00.000Z",
|
||||
"samsung chromebook plus (lte)": "2024-06-01T00:00:00.000Z",
|
||||
"samsung chromebook plus (v2)": "2024-06-01T00:00:00.000Z",
|
||||
"samsung chromebook pro": "2022-11-01T00:00:00.000Z",
|
||||
"samsung chromebook series 5": "2016-06-01T00:00:00.000Z",
|
||||
"samsung chromebook series 5 550": "2017-05-01T00:00:00.000Z",
|
||||
"samsung chromebox series 3": "2018-03-01T00:00:00.000Z",
|
||||
"sector 5 e1 rugged chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"sector 5 e3 chromebook": "2023-11-01T00:00:00.000Z",
|
||||
"senkatel c1101 chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"thinkpad 11e chromebook 3rd gen (yoga/clamshell)": "2021-06-01T00:00:00.000Z",
|
||||
"thinkpad 13 chromebook": "2022-11-01T00:00:00.000Z",
|
||||
"toshiba chromebook": "2019-06-01T00:00:00.000Z",
|
||||
"toshiba chromebook 2": "2020-06-01T00:00:00.000Z",
|
||||
"toshiba chromebook 2 (2015 edition)": "2020-09-01T00:00:00.000Z",
|
||||
"true idc chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"true idc chromebook 11": "2020-06-01T00:00:00.000Z",
|
||||
"videonet chromebook": "2020-06-01T00:00:00.000Z",
|
||||
"videonet chromebook bl10": "2020-06-01T00:00:00.000Z",
|
||||
"viewsonic nmp660 chromebox": "2024-06-01T00:00:00.000Z",
|
||||
"viglen chromebook 11": "2020-06-01T00:00:00.000Z",
|
||||
"viglen chromebook 11c": "2023-11-01T00:00:00.000Z",
|
||||
"viglen chromebook 360": "2021-08-01T00:00:00.000Z",
|
||||
"xolo chromebook": "2020-06-01T00:00:00.000Z"
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "email-settings:v2",
|
||||
"name": "email-settings",
|
||||
"version": "v2",
|
||||
"revision": "20161013",
|
||||
"title": "Email Settings API",
|
||||
"description": "Lets you manage Google Apps Email Settings",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/admin-sdk/email-settings",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://apps-apis.google.com/",
|
||||
"rootUrl": "https://apps-apis.google.com/",
|
||||
"servicePath": "/a/feeds/emailsettings/2.0/",
|
||||
"parameters": {
|
||||
"v": {
|
||||
"type": "string",
|
||||
"description": "GData Version",
|
||||
"default": "2.0",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"GData 2.0"
|
||||
],
|
||||
"location": "query"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"description": "Data format for the response.",
|
||||
"default": "json",
|
||||
"enum": [
|
||||
"json"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json"
|
||||
],
|
||||
"location": "query"
|
||||
},
|
||||
"quotaUser": {
|
||||
"type": "string",
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.",
|
||||
"location": "query"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://apps-apis.google.com/a/feeds/emailsettings/2.0/": {
|
||||
"description": "Manage email settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Delegate": {
|
||||
"id": "Delegate",
|
||||
"type": "object",
|
||||
"description": "a delegate.",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "organization name value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Delegates": {
|
||||
"id": "feed",
|
||||
"type": "object",
|
||||
"description": "List of delegates.",
|
||||
"properties": {
|
||||
"entry": {
|
||||
"type": "object",
|
||||
"description": "list of delegates",
|
||||
"items": {
|
||||
"$ref": "Delegate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"delegates": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "email-settings.delegates.get",
|
||||
"path": "{domainName}/{delegator}/delegation",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegator": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Delegates"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "email-settings.delegates.delete",
|
||||
"path": "{domainName}/{delegator}/delegation/{delegate}",
|
||||
"httpMethod": "DELETE",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegator": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegate": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ GAM installation script.
|
||||
OPTIONS:
|
||||
-h show help.
|
||||
-d Directory where gam folder will be installed. Default is \$HOME/bin/
|
||||
-a Architecture to install (i386, x86_64, arm). Default is to detect your arch with "uname -m".
|
||||
-a Architecture to install (i386, x86_64, x86_64_legacy, arm, arm64). Default is to detect your arch with "uname -m".
|
||||
-o OS we are running (linux, macos). Default is to detect your OS with "uname -s".
|
||||
-l Just upgrade GAM to latest version. Skips project creation and auth.
|
||||
-p Profile update (true, false). Should script add gam command to environment. Default is true.
|
||||
@@ -26,6 +26,8 @@ upgrade_only=false
|
||||
gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
gam_glibc_vers="2.23 2.19 2.15"
|
||||
|
||||
while getopts "hd:a:o:lp:u:r:v:" OPTION
|
||||
do
|
||||
case $OPTION in
|
||||
@@ -75,28 +77,44 @@ echo -e "\x1B[1;33m$1"
|
||||
echo -e '\x1B[0m'
|
||||
}
|
||||
|
||||
version_gt()
|
||||
{
|
||||
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
|
||||
}
|
||||
|
||||
case $gamos in
|
||||
[lL]inux)
|
||||
gamos="linux"
|
||||
this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}')
|
||||
echo "This Linux distribution uses glibc $this_glibc_ver"
|
||||
useglibc="legacy"
|
||||
for gam_glibc_ver in $gam_glibc_vers; do
|
||||
if version_gt $this_glibc_ver $gam_glibc_ver; then
|
||||
useglibc="glibc$gam_glibc_ver"
|
||||
echo_green "Using GAM compiled against $useglibc"
|
||||
break
|
||||
fi
|
||||
done
|
||||
case $gamarch in
|
||||
x86_64) gamfile="linux-x86_64.tar.xz";;
|
||||
x86_64) gamfile="linux-x86_64-$useglibc.tar.xz";;
|
||||
i?86) gamfile="linux-i686.tar.xz";;
|
||||
arm*) gamfile="linux-armv7l.tar.xz";;
|
||||
arm|armv7l) gamfile="linux-armv7l.tar.xz";;
|
||||
arm64|aarch64) gamfile="linux-aarch64.tar.xz";;
|
||||
*)
|
||||
echo_red "ERROR: this installer currently only supports i386, x86_64 and arm Linux. Looks like you're running on $gamarch. Exiting."
|
||||
echo_red "ERROR: this installer currently only supports i386, x86_64, arm and arm64 Linux. Looks like you're running on $gamarch. Exiting."
|
||||
exit
|
||||
esac
|
||||
;;
|
||||
[Mm]ac[Oo][sS]|[Dd]arwin)
|
||||
osver=$(sw_vers -productVersion | awk -F'.' '{print $2}')
|
||||
if (( $osver < 10 )); then
|
||||
echo_red "ERROR: GAM currently requires MacOS 10.10 or newer. You are running MacOS 10.$osver. Please upgrade."
|
||||
if (( $osver < 13 )); then
|
||||
echo_red "ERROR: GAM currently requires MacOS 10.13 or newer. You are running MacOS 10.$osver. Please upgrade."
|
||||
exit
|
||||
else
|
||||
echo_green "Good, you're running MacOS 10.$osver..."
|
||||
fi
|
||||
gamos="macos"
|
||||
gamfile="macos.tar.xz"
|
||||
gamfile="macos-x86_64.tar.xz"
|
||||
;;
|
||||
*)
|
||||
echo_red "Sorry, this installer currently only supports Linux and MacOS. Looks like you're runnning on $gamos. Exiting."
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
:neworupgrade
|
||||
@echo(
|
||||
@set /p adminemail= "Please enter your G Suite admin email address: "
|
||||
@set /p nu= "Is this a new install or an upgrade? [n or u] "
|
||||
@if /I "%nu%"=="u" (
|
||||
@ echo GAM installation and setup complete!
|
||||
@ goto alldone
|
||||
)
|
||||
@if /I not "%nu%"=="n" (
|
||||
@ echo(
|
||||
@ echo Please answer n or u.
|
||||
@ goto neworupgrade
|
||||
)
|
||||
|
||||
:createproject
|
||||
@echo(
|
||||
@@ -16,10 +26,12 @@
|
||||
@ echo Please answer y or n.
|
||||
@ goto createproject
|
||||
)
|
||||
@echo(
|
||||
@set /p adminemail= "Please enter your G Suite admin email address: "
|
||||
@gam create project %adminemail%
|
||||
@if not ERRORLEVEL 1 goto projectdone
|
||||
@echo(
|
||||
@echo Projection creation failed. Trying again. Say n to skip projection creation.
|
||||
@echo Project creation failed. Trying again. Say n to skip project creation.
|
||||
@goto createproject
|
||||
:projectdone
|
||||
|
||||
|
||||
13650
src/gam.py
13650
src/gam.py
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google namespace package."""
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
@@ -1,28 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google Auth Library for Python."""
|
||||
|
||||
import logging
|
||||
|
||||
from google.auth._default import default
|
||||
|
||||
|
||||
__all__ = [
|
||||
'default',
|
||||
]
|
||||
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
@@ -1,126 +0,0 @@
|
||||
# Copyright 2015 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helpers for reading the Google Cloud SDK's configuration."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from google.auth import environment_vars
|
||||
import google.oauth2.credentials
|
||||
|
||||
|
||||
# The ~/.config subdirectory containing gcloud credentials.
|
||||
_CONFIG_DIRECTORY = 'gcloud'
|
||||
# Windows systems store config at %APPDATA%\gcloud
|
||||
_WINDOWS_CONFIG_ROOT_ENV_VAR = 'APPDATA'
|
||||
# The name of the file in the Cloud SDK config that contains default
|
||||
# credentials.
|
||||
_CREDENTIALS_FILENAME = 'application_default_credentials.json'
|
||||
# The name of the Cloud SDK shell script
|
||||
_CLOUD_SDK_POSIX_COMMAND = 'gcloud'
|
||||
_CLOUD_SDK_WINDOWS_COMMAND = 'gcloud.cmd'
|
||||
# The command to get the Cloud SDK configuration
|
||||
_CLOUD_SDK_CONFIG_COMMAND = ('config', 'config-helper', '--format', 'json')
|
||||
# Cloud SDK's application-default client ID
|
||||
CLOUD_SDK_CLIENT_ID = (
|
||||
'764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com')
|
||||
|
||||
|
||||
def get_config_path():
|
||||
"""Returns the absolute path the the Cloud SDK's configuration directory.
|
||||
|
||||
Returns:
|
||||
str: The Cloud SDK config path.
|
||||
"""
|
||||
# If the path is explicitly set, return that.
|
||||
try:
|
||||
return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Non-windows systems store this at ~/.config/gcloud
|
||||
if os.name != 'nt':
|
||||
return os.path.join(
|
||||
os.path.expanduser('~'), '.config', _CONFIG_DIRECTORY)
|
||||
# Windows systems store config at %APPDATA%\gcloud
|
||||
else:
|
||||
try:
|
||||
return os.path.join(
|
||||
os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR],
|
||||
_CONFIG_DIRECTORY)
|
||||
except KeyError:
|
||||
# This should never happen unless someone is really
|
||||
# messing with things, but we'll cover the case anyway.
|
||||
drive = os.environ.get('SystemDrive', 'C:')
|
||||
return os.path.join(
|
||||
drive, '\\', _CONFIG_DIRECTORY)
|
||||
|
||||
|
||||
def get_application_default_credentials_path():
|
||||
"""Gets the path to the application default credentials file.
|
||||
|
||||
The path may or may not exist.
|
||||
|
||||
Returns:
|
||||
str: The full path to application default credentials.
|
||||
"""
|
||||
config_path = get_config_path()
|
||||
return os.path.join(config_path, _CREDENTIALS_FILENAME)
|
||||
|
||||
|
||||
def load_authorized_user_credentials(info):
|
||||
"""Loads an authorized user credential.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The loaded file's data.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: if the info is in the wrong format or missing data.
|
||||
"""
|
||||
return google.oauth2.credentials.Credentials.from_authorized_user_info(
|
||||
info)
|
||||
|
||||
|
||||
def get_project_id():
|
||||
"""Gets the project ID from the Cloud SDK.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The project ID.
|
||||
"""
|
||||
if os.name == 'nt':
|
||||
command = _CLOUD_SDK_WINDOWS_COMMAND
|
||||
else:
|
||||
command = _CLOUD_SDK_POSIX_COMMAND
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
(command,) + _CLOUD_SDK_CONFIG_COMMAND,
|
||||
stderr=subprocess.STDOUT)
|
||||
except (subprocess.CalledProcessError, OSError, IOError):
|
||||
return None
|
||||
|
||||
try:
|
||||
configuration = json.loads(output.decode('utf-8'))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
try:
|
||||
return configuration['configuration']['properties']['core']['project']
|
||||
except KeyError:
|
||||
return None
|
||||
@@ -1,306 +0,0 @@
|
||||
# Copyright 2015 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Application default credentials.
|
||||
|
||||
Implements application default credentials and project ID detection.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import six
|
||||
|
||||
from google.auth import environment_vars
|
||||
from google.auth import exceptions
|
||||
import google.auth.transport._http_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Valid types accepted for file-based credentials.
|
||||
_AUTHORIZED_USER_TYPE = 'authorized_user'
|
||||
_SERVICE_ACCOUNT_TYPE = 'service_account'
|
||||
_VALID_TYPES = (_AUTHORIZED_USER_TYPE, _SERVICE_ACCOUNT_TYPE)
|
||||
|
||||
# Help message when no credentials can be found.
|
||||
_HELP_MESSAGE = """\
|
||||
Could not automatically determine credentials. Please set {env} or \
|
||||
explicitly create credentials and re-run the application. For more \
|
||||
information, please see \
|
||||
https://developers.google.com/accounts/docs/application-default-credentials.
|
||||
""".format(env=environment_vars.CREDENTIALS).strip()
|
||||
|
||||
# Warning when using Cloud SDK user credentials
|
||||
_CLOUD_SDK_CREDENTIALS_WARNING = """\
|
||||
Your application has authenticated using end user credentials from Google \
|
||||
Cloud SDK. We recommend that most server applications use service accounts \
|
||||
instead. If your application continues to use end user credentials from Cloud \
|
||||
SDK, you might receive a "quota exceeded" or "API not enabled" error. For \
|
||||
more information about service accounts, see \
|
||||
https://cloud.google.com/docs/authentication/."""
|
||||
|
||||
|
||||
def _warn_about_problematic_credentials(credentials):
|
||||
"""Determines if the credentials are problematic.
|
||||
|
||||
Credentials from the Cloud SDK that are associated with Cloud SDK's project
|
||||
are problematic because they may not have APIs enabled and have limited
|
||||
quota. If this is the case, warn about it.
|
||||
"""
|
||||
from google.auth import _cloud_sdk
|
||||
if credentials.client_id == _cloud_sdk.CLOUD_SDK_CLIENT_ID:
|
||||
warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
|
||||
|
||||
|
||||
def _load_credentials_from_file(filename):
|
||||
"""Loads credentials from a file.
|
||||
|
||||
The credentials file must be a service account key or stored authorized
|
||||
user credentials.
|
||||
|
||||
Args:
|
||||
filename (str): The full path to the credentials file.
|
||||
|
||||
Returns:
|
||||
Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
|
||||
credentials and the project ID. Authorized user credentials do not
|
||||
have the project ID information.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.DefaultCredentialsError: if the file is in the
|
||||
wrong format or is missing.
|
||||
"""
|
||||
if not os.path.exists(filename):
|
||||
raise exceptions.DefaultCredentialsError(
|
||||
'File {} was not found.'.format(filename))
|
||||
|
||||
with io.open(filename, 'r') as file_obj:
|
||||
try:
|
||||
info = json.load(file_obj)
|
||||
except ValueError as caught_exc:
|
||||
new_exc = exceptions.DefaultCredentialsError(
|
||||
'File {} is not a valid json file.'.format(filename),
|
||||
caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
# The type key should indicate that the file is either a service account
|
||||
# credentials file or an authorized user credentials file.
|
||||
credential_type = info.get('type')
|
||||
|
||||
if credential_type == _AUTHORIZED_USER_TYPE:
|
||||
from google.auth import _cloud_sdk
|
||||
|
||||
try:
|
||||
credentials = _cloud_sdk.load_authorized_user_credentials(info)
|
||||
except ValueError as caught_exc:
|
||||
msg = 'Failed to load authorized user credentials from {}'.format(
|
||||
filename)
|
||||
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
# Authorized user credentials do not contain the project ID.
|
||||
_warn_about_problematic_credentials(credentials)
|
||||
return credentials, None
|
||||
|
||||
elif credential_type == _SERVICE_ACCOUNT_TYPE:
|
||||
from google.oauth2 import service_account
|
||||
|
||||
try:
|
||||
credentials = (
|
||||
service_account.Credentials.from_service_account_info(info))
|
||||
except ValueError as caught_exc:
|
||||
msg = 'Failed to load service account credentials from {}'.format(
|
||||
filename)
|
||||
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
return credentials, info.get('project_id')
|
||||
|
||||
else:
|
||||
raise exceptions.DefaultCredentialsError(
|
||||
'The file {file} does not have a valid type. '
|
||||
'Type is {type}, expected one of {valid_types}.'.format(
|
||||
file=filename, type=credential_type, valid_types=_VALID_TYPES))
|
||||
|
||||
|
||||
def _get_gcloud_sdk_credentials():
|
||||
"""Gets the credentials and project ID from the Cloud SDK."""
|
||||
from google.auth import _cloud_sdk
|
||||
|
||||
# Check if application default credentials exist.
|
||||
credentials_filename = (
|
||||
_cloud_sdk.get_application_default_credentials_path())
|
||||
|
||||
if not os.path.isfile(credentials_filename):
|
||||
return None, None
|
||||
|
||||
credentials, project_id = _load_credentials_from_file(
|
||||
credentials_filename)
|
||||
|
||||
if not project_id:
|
||||
project_id = _cloud_sdk.get_project_id()
|
||||
|
||||
return credentials, project_id
|
||||
|
||||
|
||||
def _get_explicit_environ_credentials():
|
||||
"""Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
||||
variable."""
|
||||
explicit_file = os.environ.get(environment_vars.CREDENTIALS)
|
||||
|
||||
if explicit_file is not None:
|
||||
credentials, project_id = _load_credentials_from_file(
|
||||
os.environ[environment_vars.CREDENTIALS])
|
||||
|
||||
return credentials, project_id
|
||||
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def _get_gae_credentials():
|
||||
"""Gets Google App Engine App Identity credentials and project ID."""
|
||||
from google.auth import app_engine
|
||||
|
||||
try:
|
||||
credentials = app_engine.Credentials()
|
||||
project_id = app_engine.get_project_id()
|
||||
return credentials, project_id
|
||||
except EnvironmentError:
|
||||
return None, None
|
||||
|
||||
|
||||
def _get_gce_credentials(request=None):
|
||||
"""Gets credentials and project ID from the GCE Metadata Service."""
|
||||
# Ping requires a transport, but we want application default credentials
|
||||
# to require no arguments. So, we'll use the _http_client transport which
|
||||
# uses http.client. This is only acceptable because the metadata server
|
||||
# doesn't do SSL and never requires proxies.
|
||||
from google.auth import compute_engine
|
||||
from google.auth.compute_engine import _metadata
|
||||
|
||||
if request is None:
|
||||
request = google.auth.transport._http_client.Request()
|
||||
|
||||
if _metadata.ping(request=request):
|
||||
# Get the project ID.
|
||||
try:
|
||||
project_id = _metadata.get_project_id(request=request)
|
||||
except exceptions.TransportError:
|
||||
project_id = None
|
||||
|
||||
return compute_engine.Credentials(), project_id
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def default(scopes=None, request=None):
|
||||
"""Gets the default credentials for the current environment.
|
||||
|
||||
`Application Default Credentials`_ provides an easy way to obtain
|
||||
credentials to call Google APIs for server-to-server or local applications.
|
||||
This function acquires credentials from the environment in the following
|
||||
order:
|
||||
|
||||
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
||||
to the path of a valid service account JSON private key file, then it is
|
||||
loaded and returned. The project ID returned is the project ID defined
|
||||
in the service account file if available (some older files do not
|
||||
contain project ID information).
|
||||
2. If the `Google Cloud SDK`_ is installed and has application default
|
||||
credentials set they are loaded and returned.
|
||||
|
||||
To enable application default credentials with the Cloud SDK run::
|
||||
|
||||
gcloud auth application-default login
|
||||
|
||||
If the Cloud SDK has an active project, the project ID is returned. The
|
||||
active project can be set using::
|
||||
|
||||
gcloud config set project
|
||||
|
||||
3. If the application is running in the `App Engine standard environment`_
|
||||
then the credentials and project ID from the `App Identity Service`_
|
||||
are used.
|
||||
4. If the application is running in `Compute Engine`_ or the
|
||||
`App Engine flexible environment`_ then the credentials and project ID
|
||||
are obtained from the `Metadata Service`_.
|
||||
5. If no credentials are found,
|
||||
:class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
|
||||
|
||||
.. _Application Default Credentials: https://developers.google.com\
|
||||
/identity/protocols/application-default-credentials
|
||||
.. _Google Cloud SDK: https://cloud.google.com/sdk
|
||||
.. _App Engine standard environment: https://cloud.google.com/appengine
|
||||
.. _App Identity Service: https://cloud.google.com/appengine/docs/python\
|
||||
/appidentity/
|
||||
.. _Compute Engine: https://cloud.google.com/compute
|
||||
.. _App Engine flexible environment: https://cloud.google.com\
|
||||
/appengine/flexible
|
||||
.. _Metadata Service: https://cloud.google.com/compute/docs\
|
||||
/storing-retrieving-metadata
|
||||
|
||||
Example::
|
||||
|
||||
import google.auth
|
||||
|
||||
credentials, project_id = google.auth.default()
|
||||
|
||||
Args:
|
||||
scopes (Sequence[str]): The list of scopes for the credentials. If
|
||||
specified, the credentials will automatically be scoped if
|
||||
necessary.
|
||||
request (google.auth.transport.Request): An object used to make
|
||||
HTTP requests. This is used to detect whether the application
|
||||
is running on Compute Engine. If not specified, then it will
|
||||
use the standard library http client to make requests.
|
||||
|
||||
Returns:
|
||||
Tuple[~google.auth.credentials.Credentials, Optional[str]]:
|
||||
the current environment's credentials and project ID. Project ID
|
||||
may be None, which indicates that the Project ID could not be
|
||||
ascertained from the environment.
|
||||
|
||||
Raises:
|
||||
~google.auth.exceptions.DefaultCredentialsError:
|
||||
If no credentials were found, or if the credentials found were
|
||||
invalid.
|
||||
"""
|
||||
from google.auth.credentials import with_scopes_if_required
|
||||
|
||||
explicit_project_id = os.environ.get(
|
||||
environment_vars.PROJECT,
|
||||
os.environ.get(environment_vars.LEGACY_PROJECT))
|
||||
|
||||
checkers = (
|
||||
_get_explicit_environ_credentials,
|
||||
_get_gcloud_sdk_credentials,
|
||||
_get_gae_credentials,
|
||||
lambda: _get_gce_credentials(request))
|
||||
|
||||
for checker in checkers:
|
||||
credentials, project_id = checker()
|
||||
if credentials is not None:
|
||||
credentials = with_scopes_if_required(credentials, scopes)
|
||||
effective_project_id = explicit_project_id or project_id
|
||||
if not effective_project_id:
|
||||
_LOGGER.warning(
|
||||
'No project ID could be determined. Consider running '
|
||||
'`gcloud config set project` or setting the %s '
|
||||
'environment variable',
|
||||
environment_vars.PROJECT)
|
||||
return credentials, effective_project_id
|
||||
|
||||
raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
|
||||
@@ -1,217 +0,0 @@
|
||||
# Copyright 2015 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import base64
|
||||
import calendar
|
||||
import datetime
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
|
||||
CLOCK_SKEW = datetime.timedelta(seconds=CLOCK_SKEW_SECS)
|
||||
|
||||
|
||||
def copy_docstring(source_class):
|
||||
"""Decorator that copies a method's docstring from another class.
|
||||
|
||||
Args:
|
||||
source_class (type): The class that has the documented method.
|
||||
|
||||
Returns:
|
||||
Callable: A decorator that will copy the docstring of the same
|
||||
named method in the source class to the decorated method.
|
||||
"""
|
||||
def decorator(method):
|
||||
"""Decorator implementation.
|
||||
|
||||
Args:
|
||||
method (Callable): The method to copy the docstring to.
|
||||
|
||||
Returns:
|
||||
Callable: the same method passed in with an updated docstring.
|
||||
|
||||
Raises:
|
||||
ValueError: if the method already has a docstring.
|
||||
"""
|
||||
if method.__doc__:
|
||||
raise ValueError('Method already has a docstring.')
|
||||
|
||||
source_method = getattr(source_class, method.__name__)
|
||||
method.__doc__ = source_method.__doc__
|
||||
|
||||
return method
|
||||
return decorator
|
||||
|
||||
|
||||
def utcnow():
|
||||
"""Returns the current UTC datetime.
|
||||
|
||||
Returns:
|
||||
datetime: The current time in UTC.
|
||||
"""
|
||||
return datetime.datetime.utcnow()
|
||||
|
||||
|
||||
def datetime_to_secs(value):
|
||||
"""Convert a datetime object to the number of seconds since the UNIX epoch.
|
||||
|
||||
Args:
|
||||
value (datetime): The datetime to convert.
|
||||
|
||||
Returns:
|
||||
int: The number of seconds since the UNIX epoch.
|
||||
"""
|
||||
return calendar.timegm(value.utctimetuple())
|
||||
|
||||
|
||||
def to_bytes(value, encoding='utf-8'):
|
||||
"""Converts a string value to bytes, if necessary.
|
||||
|
||||
Unfortunately, ``six.b`` is insufficient for this task since in
|
||||
Python 2 because it does not modify ``unicode`` objects.
|
||||
|
||||
Args:
|
||||
value (Union[str, bytes]): The value to be converted.
|
||||
encoding (str): The encoding to use to convert unicode to bytes.
|
||||
Defaults to "utf-8".
|
||||
|
||||
Returns:
|
||||
bytes: The original value converted to bytes (if unicode) or as
|
||||
passed in if it started out as bytes.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value could not be converted to bytes.
|
||||
"""
|
||||
result = (value.encode(encoding)
|
||||
if isinstance(value, six.text_type) else value)
|
||||
if isinstance(result, six.binary_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('{0!r} could not be converted to bytes'.format(value))
|
||||
|
||||
|
||||
def from_bytes(value):
|
||||
"""Converts bytes to a string value, if necessary.
|
||||
|
||||
Args:
|
||||
value (Union[str, bytes]): The value to be converted.
|
||||
|
||||
Returns:
|
||||
str: The original value converted to unicode (if bytes) or as passed in
|
||||
if it started out as unicode.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value could not be converted to unicode.
|
||||
"""
|
||||
result = (value.decode('utf-8')
|
||||
if isinstance(value, six.binary_type) else value)
|
||||
if isinstance(result, six.text_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError(
|
||||
'{0!r} could not be converted to unicode'.format(value))
|
||||
|
||||
|
||||
def update_query(url, params, remove=None):
|
||||
"""Updates a URL's query parameters.
|
||||
|
||||
Replaces any current values if they are already present in the URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL to update.
|
||||
params (Mapping[str, str]): A mapping of query parameter
|
||||
keys to values.
|
||||
remove (Sequence[str]): Parameters to remove from the query string.
|
||||
|
||||
Returns:
|
||||
str: The URL with updated query parameters.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> url = 'http://example.com?a=1'
|
||||
>>> update_query(url, {'a': '2'})
|
||||
http://example.com?a=2
|
||||
>>> update_query(url, {'b': '3'})
|
||||
http://example.com?a=1&b=3
|
||||
>> update_query(url, {'b': '3'}, remove=['a'])
|
||||
http://example.com?b=3
|
||||
|
||||
"""
|
||||
if remove is None:
|
||||
remove = []
|
||||
|
||||
# Split the URL into parts.
|
||||
parts = urllib.parse.urlparse(url)
|
||||
# Parse the query string.
|
||||
query_params = urllib.parse.parse_qs(parts.query)
|
||||
# Update the query parameters with the new parameters.
|
||||
query_params.update(params)
|
||||
# Remove any values specified in remove.
|
||||
query_params = {
|
||||
key: value for key, value
|
||||
in six.iteritems(query_params)
|
||||
if key not in remove}
|
||||
# Re-encoded the query string.
|
||||
new_query = urllib.parse.urlencode(query_params, doseq=True)
|
||||
# Unsplit the url.
|
||||
new_parts = parts._replace(query=new_query)
|
||||
return urllib.parse.urlunparse(new_parts)
|
||||
|
||||
|
||||
def scopes_to_string(scopes):
|
||||
"""Converts scope value to a string suitable for sending to OAuth 2.0
|
||||
authorization servers.
|
||||
|
||||
Args:
|
||||
scopes (Sequence[str]): The sequence of scopes to convert.
|
||||
|
||||
Returns:
|
||||
str: The scopes formatted as a single string.
|
||||
"""
|
||||
return ' '.join(scopes)
|
||||
|
||||
|
||||
def string_to_scopes(scopes):
|
||||
"""Converts stringifed scopes value to a list.
|
||||
|
||||
Args:
|
||||
scopes (Union[Sequence, str]): The string of space-separated scopes
|
||||
to convert.
|
||||
Returns:
|
||||
Sequence(str): The separated scopes.
|
||||
"""
|
||||
if not scopes:
|
||||
return []
|
||||
|
||||
return scopes.split(' ')
|
||||
|
||||
|
||||
def padded_urlsafe_b64decode(value):
|
||||
"""Decodes base64 strings lacking padding characters.
|
||||
|
||||
Google infrastructure tends to omit the base64 padding characters.
|
||||
|
||||
Args:
|
||||
value (Union[str, bytes]): The encoded value.
|
||||
|
||||
Returns:
|
||||
bytes: The decoded value
|
||||
"""
|
||||
b64string = to_bytes(value)
|
||||
padded = b64string + b'=' * (-len(b64string) % 4)
|
||||
return base64.urlsafe_b64decode(padded)
|
||||
@@ -1,170 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helpers for transitioning from oauth2client to google-auth.
|
||||
|
||||
.. warning::
|
||||
This module is private as it is intended to assist first-party downstream
|
||||
clients with the transition from oauth2client to google-auth.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
|
||||
from google.auth import _helpers
|
||||
import google.auth.app_engine
|
||||
import google.oauth2.credentials
|
||||
import google.oauth2.service_account
|
||||
|
||||
try:
|
||||
import oauth2client.client
|
||||
import oauth2client.contrib.gce
|
||||
import oauth2client.service_account
|
||||
except ImportError as caught_exc:
|
||||
six.raise_from(
|
||||
ImportError('oauth2client is not installed.'), caught_exc)
|
||||
|
||||
try:
|
||||
import oauth2client.contrib.appengine
|
||||
_HAS_APPENGINE = True
|
||||
except ImportError:
|
||||
_HAS_APPENGINE = False
|
||||
|
||||
|
||||
_CONVERT_ERROR_TMPL = (
|
||||
'Unable to convert {} to a google-auth credentials class.')
|
||||
|
||||
|
||||
def _convert_oauth2_credentials(credentials):
|
||||
"""Converts to :class:`google.oauth2.credentials.Credentials`.
|
||||
|
||||
Args:
|
||||
credentials (Union[oauth2client.client.OAuth2Credentials,
|
||||
oauth2client.client.GoogleCredentials]): The credentials to
|
||||
convert.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: The converted credentials.
|
||||
"""
|
||||
new_credentials = google.oauth2.credentials.Credentials(
|
||||
token=credentials.access_token,
|
||||
refresh_token=credentials.refresh_token,
|
||||
token_uri=credentials.token_uri,
|
||||
client_id=credentials.client_id,
|
||||
client_secret=credentials.client_secret,
|
||||
scopes=credentials.scopes)
|
||||
|
||||
new_credentials._expires = credentials.token_expiry
|
||||
|
||||
return new_credentials
|
||||
|
||||
|
||||
def _convert_service_account_credentials(credentials):
|
||||
"""Converts to :class:`google.oauth2.service_account.Credentials`.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
oauth2client.service_account.ServiceAccountCredentials,
|
||||
oauth2client.service_account._JWTAccessCredentials]): The
|
||||
credentials to convert.
|
||||
|
||||
Returns:
|
||||
google.oauth2.service_account.Credentials: The converted credentials.
|
||||
"""
|
||||
info = credentials.serialization_data.copy()
|
||||
info['token_uri'] = credentials.token_uri
|
||||
return google.oauth2.service_account.Credentials.from_service_account_info(
|
||||
info)
|
||||
|
||||
|
||||
def _convert_gce_app_assertion_credentials(credentials):
|
||||
"""Converts to :class:`google.auth.compute_engine.Credentials`.
|
||||
|
||||
Args:
|
||||
credentials (oauth2client.contrib.gce.AppAssertionCredentials): The
|
||||
credentials to convert.
|
||||
|
||||
Returns:
|
||||
google.oauth2.service_account.Credentials: The converted credentials.
|
||||
"""
|
||||
return google.auth.compute_engine.Credentials(
|
||||
service_account_email=credentials.service_account_email)
|
||||
|
||||
|
||||
def _convert_appengine_app_assertion_credentials(credentials):
|
||||
"""Converts to :class:`google.auth.app_engine.Credentials`.
|
||||
|
||||
Args:
|
||||
credentials (oauth2client.contrib.app_engine.AppAssertionCredentials):
|
||||
The credentials to convert.
|
||||
|
||||
Returns:
|
||||
google.oauth2.service_account.Credentials: The converted credentials.
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
return google.auth.app_engine.Credentials(
|
||||
scopes=_helpers.string_to_scopes(credentials.scope),
|
||||
service_account_id=credentials.service_account_id)
|
||||
|
||||
|
||||
_CLASS_CONVERSION_MAP = {
|
||||
oauth2client.client.OAuth2Credentials: _convert_oauth2_credentials,
|
||||
oauth2client.client.GoogleCredentials: _convert_oauth2_credentials,
|
||||
oauth2client.service_account.ServiceAccountCredentials:
|
||||
_convert_service_account_credentials,
|
||||
oauth2client.service_account._JWTAccessCredentials:
|
||||
_convert_service_account_credentials,
|
||||
oauth2client.contrib.gce.AppAssertionCredentials:
|
||||
_convert_gce_app_assertion_credentials,
|
||||
}
|
||||
|
||||
if _HAS_APPENGINE:
|
||||
_CLASS_CONVERSION_MAP[
|
||||
oauth2client.contrib.appengine.AppAssertionCredentials] = (
|
||||
_convert_appengine_app_assertion_credentials)
|
||||
|
||||
|
||||
def convert(credentials):
|
||||
"""Convert oauth2client credentials to google-auth credentials.
|
||||
|
||||
This class converts:
|
||||
|
||||
- :class:`oauth2client.client.OAuth2Credentials` to
|
||||
:class:`google.oauth2.credentials.Credentials`.
|
||||
- :class:`oauth2client.client.GoogleCredentials` to
|
||||
:class:`google.oauth2.credentials.Credentials`.
|
||||
- :class:`oauth2client.service_account.ServiceAccountCredentials` to
|
||||
:class:`google.oauth2.service_account.Credentials`.
|
||||
- :class:`oauth2client.service_account._JWTAccessCredentials` to
|
||||
:class:`google.oauth2.service_account.Credentials`.
|
||||
- :class:`oauth2client.contrib.gce.AppAssertionCredentials` to
|
||||
:class:`google.auth.compute_engine.Credentials`.
|
||||
- :class:`oauth2client.contrib.appengine.AppAssertionCredentials` to
|
||||
:class:`google.auth.app_engine.Credentials`.
|
||||
|
||||
Returns:
|
||||
google.auth.credentials.Credentials: The converted credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the credentials could not be converted.
|
||||
"""
|
||||
|
||||
credentials_class = type(credentials)
|
||||
|
||||
try:
|
||||
return _CLASS_CONVERSION_MAP[credentials_class](credentials)
|
||||
except KeyError as caught_exc:
|
||||
new_exc = ValueError(_CONVERT_ERROR_TMPL.format(credentials_class))
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
@@ -1,73 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helper functions for loading data from a Google service account file."""
|
||||
|
||||
import io
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
from google.auth import crypt
|
||||
|
||||
|
||||
def from_dict(data, require=None):
|
||||
"""Validates a dictionary containing Google service account data.
|
||||
|
||||
Creates and returns a :class:`google.auth.crypt.Signer` instance from the
|
||||
private key specified in the data.
|
||||
|
||||
Args:
|
||||
data (Mapping[str, str]): The service account data
|
||||
require (Sequence[str]): List of keys required to be present in the
|
||||
info.
|
||||
|
||||
Returns:
|
||||
google.auth.crypt.Signer: A signer created from the private key in the
|
||||
service account file.
|
||||
|
||||
Raises:
|
||||
ValueError: if the data was in the wrong format, or if one of the
|
||||
required keys is missing.
|
||||
"""
|
||||
keys_needed = set(require if require is not None else [])
|
||||
|
||||
missing = keys_needed.difference(six.iterkeys(data))
|
||||
|
||||
if missing:
|
||||
raise ValueError(
|
||||
'Service account info was not in the expected format, missing '
|
||||
'fields {}.'.format(', '.join(missing)))
|
||||
|
||||
# Create a signer.
|
||||
signer = crypt.RSASigner.from_service_account_info(data)
|
||||
|
||||
return signer
|
||||
|
||||
|
||||
def from_filename(filename, require=None):
|
||||
"""Reads a Google service account JSON file and returns its parsed info.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account .json file.
|
||||
require (Sequence[str]): List of keys required to be present in the
|
||||
info.
|
||||
|
||||
Returns:
|
||||
Tuple[ Mapping[str, str], google.auth.crypt.Signer ]: The verified
|
||||
info and a signer instance.
|
||||
"""
|
||||
with io.open(filename, 'r', encoding='utf-8') as json_file:
|
||||
data = json.load(json_file)
|
||||
return data, from_dict(data, require=require)
|
||||
@@ -1,154 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google App Engine standard environment support.
|
||||
|
||||
This module provides authentication and signing for applications running on App
|
||||
Engine in the standard environment using the `App Identity API`_.
|
||||
|
||||
|
||||
.. _App Identity API:
|
||||
https://cloud.google.com/appengine/docs/python/appidentity/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import credentials
|
||||
from google.auth import crypt
|
||||
|
||||
try:
|
||||
from google.appengine.api import app_identity
|
||||
except ImportError:
|
||||
app_identity = None
|
||||
|
||||
|
||||
class Signer(crypt.Signer):
|
||||
"""Signs messages using the App Engine App Identity service.
|
||||
|
||||
This can be used in place of :class:`google.auth.crypt.Signer` when
|
||||
running in the App Engine standard environment.
|
||||
"""
|
||||
|
||||
@property
|
||||
def key_id(self):
|
||||
"""Optional[str]: The key ID used to identify this private key.
|
||||
|
||||
.. warning::
|
||||
This is always ``None``. The key ID used by App Engine can not
|
||||
be reliably determined ahead of time.
|
||||
"""
|
||||
return None
|
||||
|
||||
@_helpers.copy_docstring(crypt.Signer)
|
||||
def sign(self, message):
|
||||
message = _helpers.to_bytes(message)
|
||||
_, signature = app_identity.sign_blob(message)
|
||||
return signature
|
||||
|
||||
|
||||
def get_project_id():
|
||||
"""Gets the project ID for the current App Engine application.
|
||||
|
||||
Returns:
|
||||
str: The project ID
|
||||
|
||||
Raises:
|
||||
EnvironmentError: If the App Engine APIs are unavailable.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc
|
||||
# Pylint rightfully thinks EnvironmentError is OSError, but doesn't
|
||||
# realize it's a valid alias.
|
||||
if app_identity is None:
|
||||
raise EnvironmentError(
|
||||
'The App Engine APIs are not available.')
|
||||
return app_identity.get_application_id()
|
||||
|
||||
|
||||
class Credentials(credentials.Scoped, credentials.Signing,
|
||||
credentials.Credentials):
|
||||
"""App Engine standard environment credentials.
|
||||
|
||||
These credentials use the App Engine App Identity API to obtain access
|
||||
tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, scopes=None, service_account_id=None):
|
||||
"""
|
||||
Args:
|
||||
scopes (Sequence[str]): Scopes to request from the App Identity
|
||||
API.
|
||||
service_account_id (str): The service account ID passed into
|
||||
:func:`google.appengine.api.app_identity.get_access_token`.
|
||||
If not specified, the default application service account
|
||||
ID will be used.
|
||||
|
||||
Raises:
|
||||
EnvironmentError: If the App Engine APIs are unavailable.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc
|
||||
# Pylint rightfully thinks EnvironmentError is OSError, but doesn't
|
||||
# realize it's a valid alias.
|
||||
if app_identity is None:
|
||||
raise EnvironmentError(
|
||||
'The App Engine APIs are not available.')
|
||||
|
||||
super(Credentials, self).__init__()
|
||||
self._scopes = scopes
|
||||
self._service_account_id = service_account_id
|
||||
self._signer = Signer()
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
# pylint: disable=unused-argument
|
||||
token, ttl = app_identity.get_access_token(
|
||||
self._scopes, self._service_account_id)
|
||||
expiry = datetime.datetime.utcfromtimestamp(ttl)
|
||||
|
||||
self.token, self.expiry = token, expiry
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email."""
|
||||
if self._service_account_id is None:
|
||||
self._service_account_id = app_identity.get_service_account_name()
|
||||
return self._service_account_id
|
||||
|
||||
@property
|
||||
def requires_scopes(self):
|
||||
"""Checks if the credentials requires scopes.
|
||||
|
||||
Returns:
|
||||
bool: True if there are no scopes set otherwise False.
|
||||
"""
|
||||
return not self._scopes
|
||||
|
||||
@_helpers.copy_docstring(credentials.Scoped)
|
||||
def with_scopes(self, scopes):
|
||||
return self.__class__(
|
||||
scopes=scopes, service_account_id=self._service_account_id)
|
||||
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self.service_account_email
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google Compute Engine authentication."""
|
||||
|
||||
from google.auth.compute_engine.credentials import Credentials
|
||||
from google.auth.compute_engine.credentials import IDTokenCredentials
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Credentials',
|
||||
'IDTokenCredentials',
|
||||
]
|
||||
@@ -1,204 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Provides helper methods for talking to the Compute Engine metadata server.
|
||||
|
||||
See https://cloud.google.com/compute/docs/metadata for more details.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import six
|
||||
from six.moves import http_client
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import environment_vars
|
||||
from google.auth import exceptions
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format(
|
||||
os.getenv(environment_vars.GCE_METADATA_ROOT, 'metadata.google.internal'))
|
||||
|
||||
# This is used to ping the metadata server, it avoids the cost of a DNS
|
||||
# lookup.
|
||||
_METADATA_IP_ROOT = 'http://{}'.format(
|
||||
os.getenv(environment_vars.GCE_METADATA_IP, '169.254.169.254'))
|
||||
_METADATA_FLAVOR_HEADER = 'metadata-flavor'
|
||||
_METADATA_FLAVOR_VALUE = 'Google'
|
||||
_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}
|
||||
|
||||
# Timeout in seconds to wait for the GCE metadata server when detecting the
|
||||
# GCE environment.
|
||||
try:
|
||||
_METADATA_DEFAULT_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3))
|
||||
except ValueError: # pragma: NO COVER
|
||||
_METADATA_DEFAULT_TIMEOUT = 3
|
||||
|
||||
|
||||
def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT):
|
||||
"""Checks to see if the metadata server is available.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
timeout (int): How long to wait for the metadata server to respond.
|
||||
|
||||
Returns:
|
||||
bool: True if the metadata server is reachable, False otherwise.
|
||||
"""
|
||||
# NOTE: The explicit ``timeout`` is a workaround. The underlying
|
||||
# issue is that resolving an unknown host on some networks will take
|
||||
# 20-30 seconds; making this timeout short fixes the issue, but
|
||||
# could lead to false negatives in the event that we are on GCE, but
|
||||
# the metadata resolution was particularly slow. The latter case is
|
||||
# "unlikely".
|
||||
try:
|
||||
response = request(
|
||||
url=_METADATA_IP_ROOT, method='GET', headers=_METADATA_HEADERS,
|
||||
timeout=timeout)
|
||||
|
||||
metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
|
||||
return (response.status == http_client.OK and
|
||||
metadata_flavor == _METADATA_FLAVOR_VALUE)
|
||||
|
||||
except exceptions.TransportError:
|
||||
_LOGGER.info('Compute Engine Metadata server unavailable.')
|
||||
return False
|
||||
|
||||
|
||||
def get(request, path, root=_METADATA_ROOT, recursive=False):
|
||||
"""Fetch a resource from the metadata server.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
path (str): The resource to retrieve. For example,
|
||||
``'instance/service-accounts/default'``.
|
||||
root (str): The full path to the metadata server root.
|
||||
recursive (bool): Whether to do a recursive query of metadata. See
|
||||
https://cloud.google.com/compute/docs/metadata#aggcontents for more
|
||||
details.
|
||||
|
||||
Returns:
|
||||
Union[Mapping, str]: If the metadata server returns JSON, a mapping of
|
||||
the decoded JSON is return. Otherwise, the response content is
|
||||
returned as a string.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: if an error occurred while
|
||||
retrieving metadata.
|
||||
"""
|
||||
base_url = urlparse.urljoin(root, path)
|
||||
query_params = {}
|
||||
|
||||
if recursive:
|
||||
query_params['recursive'] = 'true'
|
||||
|
||||
url = _helpers.update_query(base_url, query_params)
|
||||
|
||||
response = request(url=url, method='GET', headers=_METADATA_HEADERS)
|
||||
|
||||
if response.status == http_client.OK:
|
||||
content = _helpers.from_bytes(response.data)
|
||||
if response.headers['content-type'] == 'application/json':
|
||||
try:
|
||||
return json.loads(content)
|
||||
except ValueError as caught_exc:
|
||||
new_exc = exceptions.TransportError(
|
||||
'Received invalid JSON from the Google Compute Engine'
|
||||
'metadata service: {:.20}'.format(content))
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
else:
|
||||
return content
|
||||
else:
|
||||
raise exceptions.TransportError(
|
||||
'Failed to retrieve {} from the Google Compute Engine'
|
||||
'metadata service. Status: {} Response:\n{}'.format(
|
||||
url, response.status, response.data), response)
|
||||
|
||||
|
||||
def get_project_id(request):
|
||||
"""Get the Google Cloud Project ID from the metadata server.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
|
||||
Returns:
|
||||
str: The project ID
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: if an error occurred while
|
||||
retrieving metadata.
|
||||
"""
|
||||
return get(request, 'project/project-id')
|
||||
|
||||
|
||||
def get_service_account_info(request, service_account='default'):
|
||||
"""Get information about a service account from the metadata server.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
service_account (str): The string 'default' or a service account email
|
||||
address. The determines which service account for which to acquire
|
||||
information.
|
||||
|
||||
Returns:
|
||||
Mapping: The service account's information, for example::
|
||||
|
||||
{
|
||||
'email': '...',
|
||||
'scopes': ['scope', ...],
|
||||
'aliases': ['default', '...']
|
||||
}
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: if an error occurred while
|
||||
retrieving metadata.
|
||||
"""
|
||||
return get(
|
||||
request,
|
||||
'instance/service-accounts/{0}/'.format(service_account),
|
||||
recursive=True)
|
||||
|
||||
|
||||
def get_service_account_token(request, service_account='default'):
|
||||
"""Get the OAuth 2.0 access token for a service account.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
service_account (str): The string 'default' or a service account email
|
||||
address. The determines which service account for which to acquire
|
||||
an access token.
|
||||
|
||||
Returns:
|
||||
Union[str, datetime]: The access token and its expiration.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: if an error occurred while
|
||||
retrieving metadata.
|
||||
"""
|
||||
token_json = get(
|
||||
request,
|
||||
'instance/service-accounts/{0}/token'.format(service_account))
|
||||
token_expiry = _helpers.utcnow() + datetime.timedelta(
|
||||
seconds=token_json['expires_in'])
|
||||
return token_json['access_token'], token_expiry
|
||||
@@ -1,239 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google Compute Engine credentials.
|
||||
|
||||
This module provides authentication for application running on Google Compute
|
||||
Engine using the Compute Engine metadata server.
|
||||
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import six
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import credentials
|
||||
from google.auth import exceptions
|
||||
from google.auth import iam
|
||||
from google.auth import jwt
|
||||
from google.auth.compute_engine import _metadata
|
||||
from google.oauth2 import _client
|
||||
|
||||
|
||||
class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
|
||||
"""Compute Engine Credentials.
|
||||
|
||||
These credentials use the Google Compute Engine metadata server to obtain
|
||||
OAuth 2.0 access tokens associated with the instance's service account.
|
||||
|
||||
For more information about Compute Engine authentication, including how
|
||||
to configure scopes, see the `Compute Engine authentication
|
||||
documentation`_.
|
||||
|
||||
.. note:: Compute Engine instances can be created with scopes and therefore
|
||||
these credentials are considered to be 'scoped'. However, you can
|
||||
not use :meth:`~google.auth.credentials.ScopedCredentials.with_scopes`
|
||||
because it is not possible to change the scopes that the instance
|
||||
has. Also note that
|
||||
:meth:`~google.auth.credentials.ScopedCredentials.has_scopes` will not
|
||||
work until the credentials have been refreshed.
|
||||
|
||||
.. _Compute Engine authentication documentation:
|
||||
https://cloud.google.com/compute/docs/authentication#using
|
||||
"""
|
||||
|
||||
def __init__(self, service_account_email='default'):
|
||||
"""
|
||||
Args:
|
||||
service_account_email (str): The service account email to use, or
|
||||
'default'. A Compute Engine instance may have multiple service
|
||||
accounts.
|
||||
"""
|
||||
super(Credentials, self).__init__()
|
||||
self._service_account_email = service_account_email
|
||||
|
||||
def _retrieve_info(self, request):
|
||||
"""Retrieve information about the service account.
|
||||
|
||||
Updates the scopes and retrieves the full service account email.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
"""
|
||||
info = _metadata.get_service_account_info(
|
||||
request,
|
||||
service_account=self._service_account_email)
|
||||
|
||||
self._service_account_email = info['email']
|
||||
self._scopes = info['scopes']
|
||||
|
||||
def refresh(self, request):
|
||||
"""Refresh the access token and scopes.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the Compute Engine metadata
|
||||
service can't be reached if if the instance has not
|
||||
credentials.
|
||||
"""
|
||||
try:
|
||||
self._retrieve_info(request)
|
||||
self.token, self.expiry = _metadata.get_service_account_token(
|
||||
request,
|
||||
service_account=self._service_account_email)
|
||||
except exceptions.TransportError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email.
|
||||
|
||||
.. note: This is not guaranteed to be set until :meth`refresh` has been
|
||||
called.
|
||||
"""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def requires_scopes(self):
|
||||
"""False: Compute Engine credentials can not be scoped."""
|
||||
return False
|
||||
|
||||
|
||||
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
_DEFAULT_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
|
||||
|
||||
class IDTokenCredentials(credentials.Credentials, credentials.Signing):
|
||||
"""Open ID Connect ID Token-based service account credentials.
|
||||
|
||||
These credentials relies on the default service account of a GCE instance.
|
||||
|
||||
In order for this to work, the GCE instance must have been started with
|
||||
a service account that has access to the IAM Cloud API.
|
||||
"""
|
||||
def __init__(self, request, target_audience,
|
||||
token_uri=_DEFAULT_TOKEN_URI,
|
||||
additional_claims=None,
|
||||
service_account_email=None):
|
||||
"""
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
target_audience (str): The intended audience for these credentials,
|
||||
used when requesting the ID Token. The ID Token's ``aud`` claim
|
||||
will be set to this string.
|
||||
token_uri (str): The OAuth 2.0 Token URI.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT assertion used in the authorization grant.
|
||||
service_account_email (str): Optional explicit service account to
|
||||
use to sign JWT tokens.
|
||||
By default, this is the default GCE service account.
|
||||
"""
|
||||
super(IDTokenCredentials, self).__init__()
|
||||
|
||||
if service_account_email is None:
|
||||
sa_info = _metadata.get_service_account_info(request)
|
||||
service_account_email = sa_info['email']
|
||||
self._service_account_email = service_account_email
|
||||
|
||||
self._signer = iam.Signer(
|
||||
request=request,
|
||||
credentials=Credentials(),
|
||||
service_account_email=service_account_email)
|
||||
|
||||
self._token_uri = token_uri
|
||||
self._target_audience = target_audience
|
||||
|
||||
if additional_claims is not None:
|
||||
self._additional_claims = additional_claims
|
||||
else:
|
||||
self._additional_claims = {}
|
||||
|
||||
def with_target_audience(self, target_audience):
|
||||
"""Create a copy of these credentials with the specified target
|
||||
audience.
|
||||
Args:
|
||||
target_audience (str): The intended audience for these credentials,
|
||||
used when requesting the ID Token.
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
token_uri=self._token_uri,
|
||||
target_audience=target_audience,
|
||||
additional_claims=self._additional_claims.copy())
|
||||
|
||||
def _make_authorization_grant_assertion(self):
|
||||
"""Create the OAuth 2.0 assertion.
|
||||
This assertion is used during the OAuth 2.0 grant to acquire an
|
||||
ID token.
|
||||
Returns:
|
||||
bytes: The authorization grant assertion.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
'iat': _helpers.datetime_to_secs(now),
|
||||
'exp': _helpers.datetime_to_secs(expiry),
|
||||
# The issuer must be the service account email.
|
||||
'iss': self.service_account_email,
|
||||
# The audience must be the auth token endpoint's URI
|
||||
'aud': self._token_uri,
|
||||
# The target audience specifies which service the ID token is
|
||||
# intended for.
|
||||
'target_audience': self._target_audience
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
token = jwt.encode(self._signer, payload)
|
||||
|
||||
return token
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = _client.id_token_jwt_grant(
|
||||
request, self._token_uri, assertion)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email."""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def signer_email(self):
|
||||
return self._service_account_email
|
||||
@@ -1,322 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""Interfaces for credentials."""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
from google.auth import _helpers
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Credentials(object):
|
||||
"""Base class for all credentials.
|
||||
|
||||
All credentials have a :attr:`token` that is used for authentication and
|
||||
may also optionally set an :attr:`expiry` to indicate when the token will
|
||||
no longer be valid.
|
||||
|
||||
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
|
||||
Credentials can do this automatically before the first HTTP request in
|
||||
:meth:`before_request`.
|
||||
|
||||
Although the token and expiration will change as the credentials are
|
||||
:meth:`refreshed <refresh>` and used, credentials should be considered
|
||||
immutable. Various credentials will accept configuration such as private
|
||||
keys, scopes, and other options. These options are not changeable after
|
||||
construction. Some classes will provide mechanisms to copy the credentials
|
||||
with modifications such as :meth:`ScopedCredentials.with_scopes`.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.token = None
|
||||
"""str: The bearer token that can be used in HTTP headers to make
|
||||
authenticated requests."""
|
||||
self.expiry = None
|
||||
"""Optional[datetime]: When the token expires and is no longer valid.
|
||||
If this is None, the token is assumed to never expire."""
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
"""Checks if the credentials are expired.
|
||||
|
||||
Note that credentials can be invalid but not expired because
|
||||
Credentials with :attr:`expiry` set to None is considered to never
|
||||
expire.
|
||||
"""
|
||||
if not self.expiry:
|
||||
return False
|
||||
|
||||
# Remove 5 minutes from expiry to err on the side of reporting
|
||||
# expiration early so that we avoid the 401-refresh-retry loop.
|
||||
skewed_expiry = self.expiry - _helpers.CLOCK_SKEW
|
||||
return _helpers.utcnow() >= skewed_expiry
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""Checks the validity of the credentials.
|
||||
|
||||
This is True if the credentials have a :attr:`token` and the token
|
||||
is not :attr:`expired`.
|
||||
"""
|
||||
return self.token is not None and not self.expired
|
||||
|
||||
@abc.abstractmethod
|
||||
def refresh(self, request):
|
||||
"""Refreshes the access token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the credentials could
|
||||
not be refreshed.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc
|
||||
# (pylint doesn't recognize that this is abstract)
|
||||
raise NotImplementedError('Refresh must be implemented')
|
||||
|
||||
def apply(self, headers, token=None):
|
||||
"""Apply the token to the authentication header.
|
||||
|
||||
Args:
|
||||
headers (Mapping): The HTTP request headers.
|
||||
token (Optional[str]): If specified, overrides the current access
|
||||
token.
|
||||
"""
|
||||
headers['authorization'] = 'Bearer {}'.format(
|
||||
_helpers.from_bytes(token or self.token))
|
||||
|
||||
def before_request(self, request, method, url, headers):
|
||||
"""Performs credential-specific before request logic.
|
||||
|
||||
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
||||
apply the token to the authentication header.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
method (str): The request's HTTP method or the RPC method being
|
||||
invoked.
|
||||
url (str): The request's URI or the RPC service's URI.
|
||||
headers (Mapping): The request's headers.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
# (Subclasses may use these arguments to ascertain information about
|
||||
# the http request.)
|
||||
if not self.valid:
|
||||
self.refresh(request)
|
||||
self.apply(headers)
|
||||
|
||||
|
||||
class AnonymousCredentials(Credentials):
|
||||
"""Credentials that do not provide any authentication information.
|
||||
|
||||
These are useful in the case of services that support anonymous access or
|
||||
local service emulators that do not use credentials.
|
||||
"""
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
"""Returns `False`, anonymous credentials never expire."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""Returns `True`, anonymous credentials are always valid."""
|
||||
return True
|
||||
|
||||
def refresh(self, request):
|
||||
"""Raises :class:`ValueError``, anonymous credentials cannot be
|
||||
refreshed."""
|
||||
raise ValueError("Anonymous credentials cannot be refreshed.")
|
||||
|
||||
def apply(self, headers, token=None):
|
||||
"""Anonymous credentials do nothing to the request.
|
||||
|
||||
The optional ``token`` argument is not supported.
|
||||
|
||||
Raises:
|
||||
ValueError: If a token was specified.
|
||||
"""
|
||||
if token is not None:
|
||||
raise ValueError("Anonymous credentials don't support tokens.")
|
||||
|
||||
def before_request(self, request, method, url, headers):
|
||||
"""Anonymous credentials do nothing to the request."""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ReadOnlyScoped(object):
|
||||
"""Interface for credentials whose scopes can be queried.
|
||||
|
||||
OAuth 2.0-based credentials allow limiting access using scopes as described
|
||||
in `RFC6749 Section 3.3`_.
|
||||
If a credential class implements this interface then the credentials either
|
||||
use scopes in their implementation.
|
||||
|
||||
Some credentials require scopes in order to obtain a token. You can check
|
||||
if scoping is necessary with :attr:`requires_scopes`::
|
||||
|
||||
if credentials.requires_scopes:
|
||||
# Scoping is required.
|
||||
credentials = credentials.with_scopes(scopes=['one', 'two'])
|
||||
|
||||
Credentials that require scopes must either be constructed with scopes::
|
||||
|
||||
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
||||
|
||||
Or must copy an existing instance using :meth:`with_scopes`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
|
||||
|
||||
Some credentials have scopes but do not allow or require scopes to be set,
|
||||
these credentials can be used as-is.
|
||||
|
||||
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
"""
|
||||
def __init__(self):
|
||||
super(ReadOnlyScoped, self).__init__()
|
||||
self._scopes = None
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""Sequence[str]: the credentials' current set of scopes."""
|
||||
return self._scopes
|
||||
|
||||
@abc.abstractproperty
|
||||
def requires_scopes(self):
|
||||
"""True if these credentials require scopes to obtain an access token.
|
||||
"""
|
||||
return False
|
||||
|
||||
def has_scopes(self, scopes):
|
||||
"""Checks if the credentials have the given scopes.
|
||||
|
||||
.. warning: This method is not guaranteed to be accurate if the
|
||||
credentials are :attr:`~Credentials.invalid`.
|
||||
|
||||
Args:
|
||||
scopes (Sequence[str]): The list of scopes to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the credentials have the given scopes.
|
||||
"""
|
||||
return set(scopes).issubset(set(self._scopes or []))
|
||||
|
||||
|
||||
class Scoped(ReadOnlyScoped):
|
||||
"""Interface for credentials whose scopes can be replaced while copying.
|
||||
|
||||
OAuth 2.0-based credentials allow limiting access using scopes as described
|
||||
in `RFC6749 Section 3.3`_.
|
||||
If a credential class implements this interface then the credentials either
|
||||
use scopes in their implementation.
|
||||
|
||||
Some credentials require scopes in order to obtain a token. You can check
|
||||
if scoping is necessary with :attr:`requires_scopes`::
|
||||
|
||||
if credentials.requires_scopes:
|
||||
# Scoping is required.
|
||||
credentials = credentials.create_scoped(['one', 'two'])
|
||||
|
||||
Credentials that require scopes must either be constructed with scopes::
|
||||
|
||||
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
||||
|
||||
Or must copy an existing instance using :meth:`with_scopes`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
|
||||
|
||||
Some credentials have scopes but do not allow or require scopes to be set,
|
||||
these credentials can be used as-is.
|
||||
|
||||
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def with_scopes(self, scopes):
|
||||
"""Create a copy of these credentials with the specified scopes.
|
||||
|
||||
Args:
|
||||
scopes (Sequence[str]): The list of scopes to attach to the
|
||||
current credentials.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the credentials' scopes can not be changed.
|
||||
This can be avoided by checking :attr:`requires_scopes` before
|
||||
calling this method.
|
||||
"""
|
||||
raise NotImplementedError('This class does not require scoping.')
|
||||
|
||||
|
||||
def with_scopes_if_required(credentials, scopes):
|
||||
"""Creates a copy of the credentials with scopes if scoping is required.
|
||||
|
||||
This helper function is useful when you do not know (or care to know) the
|
||||
specific type of credentials you are using (such as when you use
|
||||
:func:`google.auth.default`). This function will call
|
||||
:meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
|
||||
the credentials require scoping. Otherwise, it will return the credentials
|
||||
as-is.
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
scope if necessary.
|
||||
scopes (Sequence[str]): The list of scopes to use.
|
||||
|
||||
Returns:
|
||||
google.auth.credentials.Credentials: Either a new set of scoped
|
||||
credentials, or the passed in credentials instance if no scoping
|
||||
was required.
|
||||
"""
|
||||
if isinstance(credentials, Scoped) and credentials.requires_scopes:
|
||||
return credentials.with_scopes(scopes)
|
||||
else:
|
||||
return credentials
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Signing(object):
|
||||
"""Interface for credentials that can cryptographically sign messages."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sign_bytes(self, message):
|
||||
"""Signs the given message.
|
||||
|
||||
Args:
|
||||
message (bytes): The message to sign.
|
||||
|
||||
Returns:
|
||||
bytes: The message's cryptographic signature.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc,redundant-returns-doc
|
||||
# (pylint doesn't recognize that this is abstract)
|
||||
raise NotImplementedError('Sign bytes must be implemented.')
|
||||
|
||||
@abc.abstractproperty
|
||||
def signer_email(self):
|
||||
"""Optional[str]: An email address that identifies the signer."""
|
||||
# pylint: disable=missing-raises-doc
|
||||
# (pylint doesn't recognize that this is abstract)
|
||||
raise NotImplementedError('Signer email must be implemented.')
|
||||
|
||||
@abc.abstractproperty
|
||||
def signer(self):
|
||||
"""google.auth.crypt.Signer: The signer used to sign bytes."""
|
||||
# pylint: disable=missing-raises-doc
|
||||
# (pylint doesn't recognize that this is abstract)
|
||||
raise NotImplementedError('Signer must be implemented.')
|
||||
@@ -1,79 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Cryptography helpers for verifying and signing messages.
|
||||
|
||||
The simplest way to verify signatures is using :func:`verify_signature`::
|
||||
|
||||
cert = open('certs.pem').read()
|
||||
valid = crypt.verify_signature(message, signature, cert)
|
||||
|
||||
If you're going to verify many messages with the same certificate, you can use
|
||||
:class:`RSAVerifier`::
|
||||
|
||||
cert = open('certs.pem').read()
|
||||
verifier = crypt.RSAVerifier.from_string(cert)
|
||||
valid = verifier.verify(message, signature)
|
||||
|
||||
To sign messages use :class:`RSASigner` with a private key::
|
||||
|
||||
private_key = open('private_key.pem').read()
|
||||
signer = crypt.RSASigner.from_string(private_key)
|
||||
signature = signer.sign(message)
|
||||
"""
|
||||
|
||||
import six
|
||||
|
||||
from google.auth.crypt import base
|
||||
from google.auth.crypt import rsa
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RSASigner',
|
||||
'RSAVerifier',
|
||||
'Signer',
|
||||
'Verifier',
|
||||
]
|
||||
|
||||
# Aliases to maintain the v1.0.0 interface, as the crypt module was split
|
||||
# into submodules.
|
||||
Signer = base.Signer
|
||||
Verifier = base.Verifier
|
||||
RSASigner = rsa.RSASigner
|
||||
RSAVerifier = rsa.RSAVerifier
|
||||
|
||||
|
||||
def verify_signature(message, signature, certs):
|
||||
"""Verify an RSA cryptographic signature.
|
||||
|
||||
Checks that the provided ``signature`` was generated from ``bytes`` using
|
||||
the private key associated with the ``cert``.
|
||||
|
||||
Args:
|
||||
message (Union[str, bytes]): The plaintext message.
|
||||
signature (Union[str, bytes]): The cryptographic signature to check.
|
||||
certs (Union[Sequence, str, bytes]): The certificate or certificates
|
||||
to use to check the signature.
|
||||
|
||||
Returns:
|
||||
bool: True if the signature is valid, otherwise False.
|
||||
"""
|
||||
if isinstance(certs, (six.text_type, six.binary_type)):
|
||||
certs = [certs]
|
||||
|
||||
for cert in certs:
|
||||
verifier = rsa.RSAVerifier.from_string(cert)
|
||||
if verifier.verify(message, signature):
|
||||
return True
|
||||
return False
|
||||
@@ -1,149 +0,0 @@
|
||||
# Copyright 2017 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""RSA verifier and signer that use the ``cryptography`` library.
|
||||
|
||||
This is a much faster implementation than the default (in
|
||||
``google.auth.crypt._python_rsa``), which depends on the pure-Python
|
||||
``rsa`` library.
|
||||
"""
|
||||
|
||||
import cryptography.exceptions
|
||||
from cryptography.hazmat import backends
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
import cryptography.x509
|
||||
import pkg_resources
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth.crypt import base
|
||||
|
||||
_IMPORT_ERROR_MSG = (
|
||||
'cryptography>=1.4.0 is required to use cryptography-based RSA '
|
||||
'implementation.')
|
||||
|
||||
try: # pragma: NO COVER
|
||||
release = pkg_resources.get_distribution('cryptography').parsed_version
|
||||
if release < pkg_resources.parse_version('1.4.0'):
|
||||
raise ImportError(_IMPORT_ERROR_MSG)
|
||||
except pkg_resources.DistributionNotFound: # pragma: NO COVER
|
||||
raise ImportError(_IMPORT_ERROR_MSG)
|
||||
|
||||
|
||||
_CERTIFICATE_MARKER = b'-----BEGIN CERTIFICATE-----'
|
||||
_BACKEND = backends.default_backend()
|
||||
_PADDING = padding.PKCS1v15()
|
||||
_SHA256 = hashes.SHA256()
|
||||
|
||||
|
||||
class RSAVerifier(base.Verifier):
|
||||
"""Verifies RSA cryptographic signatures using public keys.
|
||||
|
||||
Args:
|
||||
public_key (
|
||||
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
||||
The public key used to verify signatures.
|
||||
"""
|
||||
|
||||
def __init__(self, public_key):
|
||||
self._pubkey = public_key
|
||||
|
||||
@_helpers.copy_docstring(base.Verifier)
|
||||
def verify(self, message, signature):
|
||||
message = _helpers.to_bytes(message)
|
||||
try:
|
||||
self._pubkey.verify(signature, message, _PADDING, _SHA256)
|
||||
return True
|
||||
except (ValueError, cryptography.exceptions.InvalidSignature):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, public_key):
|
||||
"""Construct an Verifier instance from a public key or public
|
||||
certificate string.
|
||||
|
||||
Args:
|
||||
public_key (Union[str, bytes]): The public key in PEM format or the
|
||||
x509 public key certificate.
|
||||
|
||||
Returns:
|
||||
Verifier: The constructed verifier.
|
||||
|
||||
Raises:
|
||||
ValueError: If the public key can't be parsed.
|
||||
"""
|
||||
public_key_data = _helpers.to_bytes(public_key)
|
||||
|
||||
if _CERTIFICATE_MARKER in public_key_data:
|
||||
cert = cryptography.x509.load_pem_x509_certificate(
|
||||
public_key_data, _BACKEND)
|
||||
pubkey = cert.public_key()
|
||||
|
||||
else:
|
||||
pubkey = serialization.load_pem_public_key(
|
||||
public_key_data, _BACKEND)
|
||||
|
||||
return cls(pubkey)
|
||||
|
||||
|
||||
class RSASigner(base.Signer, base.FromServiceAccountMixin):
|
||||
"""Signs messages with an RSA private key.
|
||||
|
||||
Args:
|
||||
private_key (
|
||||
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
The private key to sign with.
|
||||
key_id (str): Optional key ID used to identify this private key. This
|
||||
can be useful to associate the private key with its associated
|
||||
public key or certificate.
|
||||
"""
|
||||
|
||||
def __init__(self, private_key, key_id=None):
|
||||
self._key = private_key
|
||||
self._key_id = key_id
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(base.Signer)
|
||||
def key_id(self):
|
||||
return self._key_id
|
||||
|
||||
@_helpers.copy_docstring(base.Signer)
|
||||
def sign(self, message):
|
||||
message = _helpers.to_bytes(message)
|
||||
return self._key.sign(
|
||||
message, _PADDING, _SHA256)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key, key_id=None):
|
||||
"""Construct a RSASigner from a private key in PEM format.
|
||||
|
||||
Args:
|
||||
key (Union[bytes, str]): Private key in PEM format.
|
||||
key_id (str): An optional key id used to identify the private key.
|
||||
|
||||
Returns:
|
||||
google.auth.crypt._cryptography_rsa.RSASigner: The
|
||||
constructed signer.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
|
||||
UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
|
||||
into a UTF-8 ``str``.
|
||||
ValueError: If ``cryptography`` "Could not deserialize key data."
|
||||
"""
|
||||
key = _helpers.to_bytes(key)
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key, password=None, backend=_BACKEND)
|
||||
return cls(private_key, key_id=key_id)
|
||||
@@ -1,176 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Pure-Python RSA cryptography implementation.
|
||||
|
||||
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
|
||||
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
|
||||
certificates. There is no support for p12 files.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import pem
|
||||
from pyasn1_modules.rfc2459 import Certificate
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
import six
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth.crypt import base
|
||||
|
||||
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
|
||||
_CERTIFICATE_MARKER = b'-----BEGIN CERTIFICATE-----'
|
||||
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
|
||||
'-----END RSA PRIVATE KEY-----')
|
||||
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
|
||||
'-----END PRIVATE KEY-----')
|
||||
_PKCS8_SPEC = PrivateKeyInfo()
|
||||
|
||||
|
||||
def _bit_list_to_bytes(bit_list):
|
||||
"""Converts an iterable of 1s and 0s to bytes.
|
||||
|
||||
Combines the list 8 at a time, treating each group of 8 bits
|
||||
as a single byte.
|
||||
|
||||
Args:
|
||||
bit_list (Sequence): Sequence of 1s and 0s.
|
||||
|
||||
Returns:
|
||||
bytes: The decoded bytes.
|
||||
"""
|
||||
num_bits = len(bit_list)
|
||||
byte_vals = bytearray()
|
||||
for start in six.moves.xrange(0, num_bits, 8):
|
||||
curr_bits = bit_list[start:start + 8]
|
||||
char_val = sum(
|
||||
val * digit for val, digit in six.moves.zip(_POW2, curr_bits))
|
||||
byte_vals.append(char_val)
|
||||
return bytes(byte_vals)
|
||||
|
||||
|
||||
class RSAVerifier(base.Verifier):
|
||||
"""Verifies RSA cryptographic signatures using public keys.
|
||||
|
||||
Args:
|
||||
public_key (rsa.key.PublicKey): The public key used to verify
|
||||
signatures.
|
||||
"""
|
||||
|
||||
def __init__(self, public_key):
|
||||
self._pubkey = public_key
|
||||
|
||||
@_helpers.copy_docstring(base.Verifier)
|
||||
def verify(self, message, signature):
|
||||
message = _helpers.to_bytes(message)
|
||||
try:
|
||||
return rsa.pkcs1.verify(message, signature, self._pubkey)
|
||||
except (ValueError, rsa.pkcs1.VerificationError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, public_key):
|
||||
"""Construct an Verifier instance from a public key or public
|
||||
certificate string.
|
||||
|
||||
Args:
|
||||
public_key (Union[str, bytes]): The public key in PEM format or the
|
||||
x509 public key certificate.
|
||||
|
||||
Returns:
|
||||
Verifier: The constructed verifier.
|
||||
|
||||
Raises:
|
||||
ValueError: If the public_key can't be parsed.
|
||||
"""
|
||||
public_key = _helpers.to_bytes(public_key)
|
||||
is_x509_cert = _CERTIFICATE_MARKER in public_key
|
||||
|
||||
# If this is a certificate, extract the public key info.
|
||||
if is_x509_cert:
|
||||
der = rsa.pem.load_pem(public_key, 'CERTIFICATE')
|
||||
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
|
||||
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
|
||||
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
|
||||
else:
|
||||
pubkey = rsa.PublicKey.load_pkcs1(public_key, 'PEM')
|
||||
return cls(pubkey)
|
||||
|
||||
|
||||
class RSASigner(base.Signer, base.FromServiceAccountMixin):
|
||||
"""Signs messages with an RSA private key.
|
||||
|
||||
Args:
|
||||
private_key (rsa.key.PrivateKey): The private key to sign with.
|
||||
key_id (str): Optional key ID used to identify this private key. This
|
||||
can be useful to associate the private key with its associated
|
||||
public key or certificate.
|
||||
"""
|
||||
|
||||
def __init__(self, private_key, key_id=None):
|
||||
self._key = private_key
|
||||
self._key_id = key_id
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(base.Signer)
|
||||
def key_id(self):
|
||||
return self._key_id
|
||||
|
||||
@_helpers.copy_docstring(base.Signer)
|
||||
def sign(self, message):
|
||||
message = _helpers.to_bytes(message)
|
||||
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key, key_id=None):
|
||||
"""Construct an Signer instance from a private key in PEM format.
|
||||
|
||||
Args:
|
||||
key (str): Private key in PEM format.
|
||||
key_id (str): An optional key id used to identify the private key.
|
||||
|
||||
Returns:
|
||||
google.auth.crypt.Signer: The constructed signer.
|
||||
|
||||
Raises:
|
||||
ValueError: If the key cannot be parsed as PKCS#1 or PKCS#8 in
|
||||
PEM format.
|
||||
"""
|
||||
key = _helpers.from_bytes(key) # PEM expects str in Python 3
|
||||
marker_id, key_bytes = pem.readPemBlocksFromFile(
|
||||
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
|
||||
|
||||
# Key is in pkcs1 format.
|
||||
if marker_id == 0:
|
||||
private_key = rsa.key.PrivateKey.load_pkcs1(
|
||||
key_bytes, format='DER')
|
||||
# Key is in pkcs8.
|
||||
elif marker_id == 1:
|
||||
key_info, remaining = decoder.decode(
|
||||
key_bytes, asn1Spec=_PKCS8_SPEC)
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
private_key_info = key_info.getComponentByName('privateKey')
|
||||
private_key = rsa.key.PrivateKey.load_pkcs1(
|
||||
private_key_info.asOctets(), format='DER')
|
||||
else:
|
||||
raise ValueError('No key could be detected.')
|
||||
|
||||
return cls(private_key, key_id=key_id)
|
||||
@@ -1,131 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Base classes for cryptographic signers and verifiers."""
|
||||
|
||||
import abc
|
||||
import io
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
|
||||
_JSON_FILE_PRIVATE_KEY = 'private_key'
|
||||
_JSON_FILE_PRIVATE_KEY_ID = 'private_key_id'
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Verifier(object):
|
||||
"""Abstract base class for crytographic signature verifiers."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a cryptographic signature.
|
||||
|
||||
Args:
|
||||
message (Union[str, bytes]): The message to verify.
|
||||
signature (Union[str, bytes]): The cryptography signature to check.
|
||||
|
||||
Returns:
|
||||
bool: True if message was signed by the private key associated
|
||||
with the public key that this object was constructed with.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc,redundant-returns-doc
|
||||
# (pylint doesn't recognize that this is abstract)
|
||||
raise NotImplementedError('Verify must be implemented')
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Signer(object):
|
||||
"""Abstract base class for cryptographic signers."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def key_id(self):
|
||||
"""Optional[str]: The key ID used to identify this private key."""
|
||||
raise NotImplementedError('Key id must be implemented')
|
||||
|
||||
@abc.abstractmethod
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message (Union[str, bytes]): The message to be signed.
|
||||
|
||||
Returns:
|
||||
bytes: The signature of the message.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc,redundant-returns-doc
|
||||
# (pylint doesn't recognize that this is abstract)
|
||||
raise NotImplementedError('Sign must be implemented')
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class FromServiceAccountMixin(object):
|
||||
"""Mix-in to enable factory constructors for a Signer."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def from_string(cls, key, key_id=None):
|
||||
"""Construct an Signer instance from a private key string.
|
||||
|
||||
Args:
|
||||
key (str): Private key as a string.
|
||||
key_id (str): An optional key id used to identify the private key.
|
||||
|
||||
Returns:
|
||||
google.auth.crypt.Signer: The constructed signer.
|
||||
|
||||
Raises:
|
||||
ValueError: If the key cannot be parsed.
|
||||
"""
|
||||
raise NotImplementedError('from_string must be implemented')
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info):
|
||||
"""Creates a Signer instance instance from a dictionary containing
|
||||
service account info in Google format.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
|
||||
Returns:
|
||||
google.auth.crypt.Signer: The constructed signer.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
if _JSON_FILE_PRIVATE_KEY not in info:
|
||||
raise ValueError(
|
||||
'The private_key field was not found in the service account '
|
||||
'info.')
|
||||
|
||||
return cls.from_string(
|
||||
info[_JSON_FILE_PRIVATE_KEY],
|
||||
info.get(_JSON_FILE_PRIVATE_KEY_ID))
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename):
|
||||
"""Creates a Signer instance from a service account .json file
|
||||
in Google format.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account .json file.
|
||||
|
||||
Returns:
|
||||
google.auth.crypt.Signer: The constructed signer.
|
||||
"""
|
||||
with io.open(filename, 'r', encoding='utf-8') as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
return cls.from_service_account_info(data)
|
||||
@@ -1,30 +0,0 @@
|
||||
# Copyright 2017 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""RSA cryptography signer and verifier."""
|
||||
|
||||
|
||||
try:
|
||||
# Prefer cryptograph-based RSA implementation.
|
||||
from google.auth.crypt import _cryptography_rsa
|
||||
|
||||
RSASigner = _cryptography_rsa.RSASigner
|
||||
RSAVerifier = _cryptography_rsa.RSAVerifier
|
||||
except ImportError: # pragma: NO COVER
|
||||
# Fallback to pure-python RSA implementation if cryptography is
|
||||
# unavailable.
|
||||
from google.auth.crypt import _python_rsa
|
||||
|
||||
RSASigner = _python_rsa.RSASigner
|
||||
RSAVerifier = _python_rsa.RSAVerifier
|
||||
@@ -1,49 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Environment variables used by :mod:`google.auth`."""
|
||||
|
||||
|
||||
PROJECT = 'GOOGLE_CLOUD_PROJECT'
|
||||
"""Environment variable defining default project.
|
||||
|
||||
This used by :func:`google.auth.default` to explicitly set a project ID. This
|
||||
environment variable is also used by the Google Cloud Python Library.
|
||||
"""
|
||||
|
||||
LEGACY_PROJECT = 'GCLOUD_PROJECT'
|
||||
"""Previously used environment variable defining the default project.
|
||||
|
||||
This environment variable is used instead of the current one in some
|
||||
situations (such as Google App Engine).
|
||||
"""
|
||||
|
||||
CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
|
||||
"""Environment variable defining the location of Google application default
|
||||
credentials."""
|
||||
|
||||
# The environment variable name which can replace ~/.config if set.
|
||||
CLOUD_SDK_CONFIG_DIR = 'CLOUDSDK_CONFIG'
|
||||
"""Environment variable defines the location of Google Cloud SDK's config
|
||||
files."""
|
||||
|
||||
# These two variables allow for customization of the addresses used when
|
||||
# contacting the GCE metadata service.
|
||||
GCE_METADATA_ROOT = 'GCE_METADATA_ROOT'
|
||||
"""Environment variable providing an alternate hostname or host:port to be
|
||||
used for GCE metadata requests."""
|
||||
|
||||
GCE_METADATA_IP = 'GCE_METADATA_IP'
|
||||
"""Environment variable providing an alternate ip:port to be used for ip-only
|
||||
GCE metadata requests."""
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Exceptions used in the google.auth package."""
|
||||
|
||||
|
||||
class GoogleAuthError(Exception):
|
||||
"""Base class for all google.auth errors."""
|
||||
|
||||
|
||||
class TransportError(GoogleAuthError):
|
||||
"""Used to indicate an error occurred during an HTTP request."""
|
||||
|
||||
|
||||
class RefreshError(GoogleAuthError):
|
||||
"""Used to indicate that an refreshing the credentials' access token
|
||||
failed."""
|
||||
|
||||
|
||||
class DefaultCredentialsError(GoogleAuthError):
|
||||
"""Used to indicate that acquiring default credentials failed."""
|
||||
@@ -1,102 +0,0 @@
|
||||
# Copyright 2017 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Tools for using the Google `Cloud Identity and Access Management (IAM)
|
||||
API`_'s auth-related functionality.
|
||||
|
||||
.. _Cloud Identity and Access Management (IAM) API:
|
||||
https://cloud.google.com/iam/docs/
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
from six.moves import http_client
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import crypt
|
||||
from google.auth import exceptions
|
||||
|
||||
_IAM_API_ROOT_URI = 'https://iam.googleapis.com/v1'
|
||||
_SIGN_BLOB_URI = (
|
||||
_IAM_API_ROOT_URI + '/projects/-/serviceAccounts/{}:signBlob?alt=json')
|
||||
|
||||
|
||||
class Signer(crypt.Signer):
|
||||
"""Signs messages using the IAM `signBlob API`_.
|
||||
|
||||
This is useful when you need to sign bytes but do not have access to the
|
||||
credential's private key file.
|
||||
|
||||
.. _signBlob API:
|
||||
https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts
|
||||
/signBlob
|
||||
"""
|
||||
|
||||
def __init__(self, request, credentials, service_account_email):
|
||||
"""
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
credentials (google.auth.credentials.Credentials): The credentials
|
||||
that will be used to authenticate the request to the IAM API.
|
||||
The credentials must have of one the following scopes:
|
||||
|
||||
- https://www.googleapis.com/auth/iam
|
||||
- https://www.googleapis.com/auth/cloud-platform
|
||||
service_account_email (str): The service account email identifying
|
||||
which service account to use to sign bytes. Often, this can
|
||||
be the same as the service account email in the given
|
||||
credentials.
|
||||
"""
|
||||
self._request = request
|
||||
self._credentials = credentials
|
||||
self._service_account_email = service_account_email
|
||||
|
||||
def _make_signing_request(self, message):
|
||||
"""Makes a request to the API signBlob API."""
|
||||
message = _helpers.to_bytes(message)
|
||||
|
||||
method = 'POST'
|
||||
url = _SIGN_BLOB_URI.format(self._service_account_email)
|
||||
headers = {}
|
||||
body = json.dumps({
|
||||
'bytesToSign': base64.b64encode(message).decode('utf-8'),
|
||||
})
|
||||
|
||||
self._credentials.before_request(self._request, method, url, headers)
|
||||
response = self._request(
|
||||
url=url, method=method, body=body, headers=headers)
|
||||
|
||||
if response.status != http_client.OK:
|
||||
raise exceptions.TransportError(
|
||||
'Error calling the IAM signBytes API: {}'.format(
|
||||
response.data))
|
||||
|
||||
return json.loads(response.data.decode('utf-8'))
|
||||
|
||||
@property
|
||||
def key_id(self):
|
||||
"""Optional[str]: The key ID used to identify this private key.
|
||||
|
||||
.. warning::
|
||||
This is always ``None``. The key ID used by IAM can not
|
||||
be reliably determined ahead of time.
|
||||
"""
|
||||
return None
|
||||
|
||||
@_helpers.copy_docstring(crypt.Signer)
|
||||
def sign(self, message):
|
||||
response = self._make_signing_request(message)
|
||||
return base64.b64decode(response['signature'])
|
||||
@@ -1,757 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""JSON Web Tokens
|
||||
|
||||
Provides support for creating (encoding) and verifying (decoding) JWTs,
|
||||
especially JWTs generated and consumed by Google infrastructure.
|
||||
|
||||
See `rfc7519`_ for more details on JWTs.
|
||||
|
||||
To encode a JWT use :func:`encode`::
|
||||
|
||||
from google.auth import crypt
|
||||
from google.auth import jwt
|
||||
|
||||
signer = crypt.Signer(private_key)
|
||||
payload = {'some': 'payload'}
|
||||
encoded = jwt.encode(signer, payload)
|
||||
|
||||
To decode a JWT and verify claims use :func:`decode`::
|
||||
|
||||
claims = jwt.decode(encoded, certs=public_certs)
|
||||
|
||||
You can also skip verification::
|
||||
|
||||
claims = jwt.decode(encoded, verify=False)
|
||||
|
||||
.. _rfc7519: https://tools.ietf.org/html/rfc7519
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import cachetools
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import _service_account_info
|
||||
from google.auth import crypt
|
||||
from google.auth import exceptions
|
||||
import google.auth.credentials
|
||||
|
||||
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
_DEFAULT_MAX_CACHE_SIZE = 10
|
||||
|
||||
|
||||
def encode(signer, payload, header=None, key_id=None):
|
||||
"""Make a signed JWT.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign the JWT.
|
||||
payload (Mapping[str, str]): The JWT payload.
|
||||
header (Mapping[str, str]): Additional JWT header payload.
|
||||
key_id (str): The key id to add to the JWT header. If the
|
||||
signer has a key id it will be used as the default. If this is
|
||||
specified it will override the signer's key id.
|
||||
|
||||
Returns:
|
||||
bytes: The encoded JWT.
|
||||
"""
|
||||
if header is None:
|
||||
header = {}
|
||||
|
||||
if key_id is None:
|
||||
key_id = signer.key_id
|
||||
|
||||
header.update({'typ': 'JWT', 'alg': 'RS256'})
|
||||
|
||||
if key_id is not None:
|
||||
header['kid'] = key_id
|
||||
|
||||
segments = [
|
||||
base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')),
|
||||
base64.urlsafe_b64encode(json.dumps(payload).encode('utf-8')),
|
||||
]
|
||||
|
||||
signing_input = b'.'.join(segments)
|
||||
signature = signer.sign(signing_input)
|
||||
segments.append(base64.urlsafe_b64encode(signature))
|
||||
|
||||
return b'.'.join(segments)
|
||||
|
||||
|
||||
def _decode_jwt_segment(encoded_section):
|
||||
"""Decodes a single JWT segment."""
|
||||
section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
|
||||
try:
|
||||
return json.loads(section_bytes.decode('utf-8'))
|
||||
except ValueError as caught_exc:
|
||||
new_exc = ValueError('Can\'t parse segment: {0}'.format(section_bytes))
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
|
||||
def _unverified_decode(token):
|
||||
"""Decodes a token and does no verification.
|
||||
|
||||
Args:
|
||||
token (Union[str, bytes]): The encoded JWT.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, str, str]: header, payload, signed_section, and
|
||||
signature.
|
||||
|
||||
Raises:
|
||||
ValueError: if there are an incorrect amount of segments in the token.
|
||||
"""
|
||||
token = _helpers.to_bytes(token)
|
||||
|
||||
if token.count(b'.') != 2:
|
||||
raise ValueError(
|
||||
'Wrong number of segments in token: {0}'.format(token))
|
||||
|
||||
encoded_header, encoded_payload, signature = token.split(b'.')
|
||||
signed_section = encoded_header + b'.' + encoded_payload
|
||||
signature = _helpers.padded_urlsafe_b64decode(signature)
|
||||
|
||||
# Parse segments
|
||||
header = _decode_jwt_segment(encoded_header)
|
||||
payload = _decode_jwt_segment(encoded_payload)
|
||||
|
||||
return header, payload, signed_section, signature
|
||||
|
||||
|
||||
def decode_header(token):
|
||||
"""Return the decoded header of a token.
|
||||
|
||||
No verification is done. This is useful to extract the key id from
|
||||
the header in order to acquire the appropriate certificate to verify
|
||||
the token.
|
||||
|
||||
Args:
|
||||
token (Union[str, bytes]): the encoded JWT.
|
||||
|
||||
Returns:
|
||||
Mapping: The decoded JWT header.
|
||||
"""
|
||||
header, _, _, _ = _unverified_decode(token)
|
||||
return header
|
||||
|
||||
|
||||
def _verify_iat_and_exp(payload):
|
||||
"""Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
|
||||
payload.
|
||||
|
||||
Args:
|
||||
payload (Mapping[str, str]): The JWT payload.
|
||||
|
||||
Raises:
|
||||
ValueError: if any checks failed.
|
||||
"""
|
||||
now = _helpers.datetime_to_secs(_helpers.utcnow())
|
||||
|
||||
# Make sure the iat and exp claims are present.
|
||||
for key in ('iat', 'exp'):
|
||||
if key not in payload:
|
||||
raise ValueError(
|
||||
'Token does not contain required claim {}'.format(key))
|
||||
|
||||
# Make sure the token wasn't issued in the future.
|
||||
iat = payload['iat']
|
||||
# Err on the side of accepting a token that is slightly early to account
|
||||
# for clock skew.
|
||||
earliest = iat - _helpers.CLOCK_SKEW_SECS
|
||||
if now < earliest:
|
||||
raise ValueError('Token used too early, {} < {}'.format(now, iat))
|
||||
|
||||
# Make sure the token wasn't issued in the past.
|
||||
exp = payload['exp']
|
||||
# Err on the side of accepting a token that is slightly out of date
|
||||
# to account for clow skew.
|
||||
latest = exp + _helpers.CLOCK_SKEW_SECS
|
||||
if latest < now:
|
||||
raise ValueError('Token expired, {} < {}'.format(latest, now))
|
||||
|
||||
|
||||
def decode(token, certs=None, verify=True, audience=None):
|
||||
"""Decode and verify a JWT.
|
||||
|
||||
Args:
|
||||
token (str): The encoded JWT.
|
||||
certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
|
||||
certificate used to validate the JWT signatyre. If bytes or string,
|
||||
it must the the public key certificate in PEM format. If a mapping,
|
||||
it must be a mapping of key IDs to public key certificates in PEM
|
||||
format. The mapping must contain the same key ID that's specified
|
||||
in the token's header.
|
||||
verify (bool): Whether to perform signature and claim validation.
|
||||
Verification is done by default.
|
||||
audience (str): The audience claim, 'aud', that this JWT should
|
||||
contain. If None then the JWT's 'aud' parameter is not verified.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: The deserialized JSON payload in the JWT.
|
||||
|
||||
Raises:
|
||||
ValueError: if any verification checks failed.
|
||||
"""
|
||||
header, payload, signed_section, signature = _unverified_decode(token)
|
||||
|
||||
if not verify:
|
||||
return payload
|
||||
|
||||
# If certs is specified as a dictionary of key IDs to certificates, then
|
||||
# use the certificate identified by the key ID in the token header.
|
||||
if isinstance(certs, collections.Mapping):
|
||||
key_id = header.get('kid')
|
||||
if key_id:
|
||||
if key_id not in certs:
|
||||
raise ValueError(
|
||||
'Certificate for key id {} not found.'.format(key_id))
|
||||
certs_to_check = [certs[key_id]]
|
||||
# If there's no key id in the header, check against all of the certs.
|
||||
else:
|
||||
certs_to_check = certs.values()
|
||||
else:
|
||||
certs_to_check = certs
|
||||
|
||||
# Verify that the signature matches the message.
|
||||
if not crypt.verify_signature(signed_section, signature, certs_to_check):
|
||||
raise ValueError('Could not verify token signature.')
|
||||
|
||||
# Verify the issued at and created times in the payload.
|
||||
_verify_iat_and_exp(payload)
|
||||
|
||||
# Check audience.
|
||||
if audience is not None:
|
||||
claim_audience = payload.get('aud')
|
||||
if audience != claim_audience:
|
||||
raise ValueError(
|
||||
'Token has wrong audience {}, expected {}'.format(
|
||||
claim_audience, audience))
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class Credentials(google.auth.credentials.Signing,
|
||||
google.auth.credentials.Credentials):
|
||||
"""Credentials that use a JWT as the bearer token.
|
||||
|
||||
These credentials require an "audience" claim. This claim identifies the
|
||||
intended recipient of the bearer token.
|
||||
|
||||
The constructor arguments determine the claims for the JWT that is
|
||||
sent with requests. Usually, you'll construct these credentials with
|
||||
one of the helper constructors as shown in the next section.
|
||||
|
||||
To create JWT credentials using a Google service account private key
|
||||
JSON file::
|
||||
|
||||
audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
|
||||
credentials = jwt.Credentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
audience=audience)
|
||||
|
||||
If you already have the service account file loaded and parsed::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = jwt.Credentials.from_service_account_info(
|
||||
service_account_info,
|
||||
audience=audience)
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify the JWT claims::
|
||||
|
||||
credentials = jwt.Credentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
audience=audience,
|
||||
additional_claims={'meta': 'data'})
|
||||
|
||||
You can also construct the credentials directly if you have a
|
||||
:class:`~google.auth.crypt.Signer` instance::
|
||||
|
||||
credentials = jwt.Credentials(
|
||||
signer,
|
||||
issuer='your-issuer',
|
||||
subject='your-subject',
|
||||
audience=audience)
|
||||
|
||||
The claims are considered immutable. If you want to modify the claims,
|
||||
you can easily create another instance using :meth:`with_claims`::
|
||||
|
||||
new_audience = (
|
||||
'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
|
||||
new_credentials = credentials.with_claims(audience=new_audience)
|
||||
"""
|
||||
|
||||
def __init__(self, signer, issuer, subject, audience,
|
||||
additional_claims=None,
|
||||
token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
issuer (str): The `iss` claim.
|
||||
subject (str): The `sub` claim.
|
||||
audience (str): the `aud` claim. The intended audience for the
|
||||
credentials.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT payload.
|
||||
token_lifetime (int): The amount of time in seconds for
|
||||
which the token is valid. Defaults to 1 hour.
|
||||
"""
|
||||
super(Credentials, self).__init__()
|
||||
self._signer = signer
|
||||
self._issuer = issuer
|
||||
self._subject = subject
|
||||
self._audience = audience
|
||||
self._token_lifetime = token_lifetime
|
||||
|
||||
if additional_claims is None:
|
||||
additional_claims = {}
|
||||
|
||||
self._additional_claims = additional_claims
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info, **kwargs):
|
||||
"""Creates a Credentials instance from a signer and service account
|
||||
info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
kwargs.setdefault('subject', info['client_email'])
|
||||
kwargs.setdefault('issuer', info['client_email'])
|
||||
return cls(signer, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info, **kwargs):
|
||||
"""Creates an Credentials instance from a dictionary.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info, require=['client_email'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename, **kwargs):
|
||||
"""Creates a Credentials instance from a service account .json file
|
||||
in Google format.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account .json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: The constructed credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename, require=['client_email'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_signing_credentials(cls, credentials, audience, **kwargs):
|
||||
"""Creates a new :class:`google.auth.jwt.Credentials` instance from an
|
||||
existing :class:`google.auth.credentials.Signing` instance.
|
||||
|
||||
The new instance will use the same signer as the existing instance and
|
||||
will use the existing instance's signer email as the issuer and
|
||||
subject by default.
|
||||
|
||||
Example::
|
||||
|
||||
svc_creds = service_account.Credentials.from_service_account_file(
|
||||
'service_account.json')
|
||||
audience = (
|
||||
'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
|
||||
jwt_creds = jwt.Credentials.from_signing_credentials(
|
||||
svc_creds, audience=audience)
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Signing): The credentials to
|
||||
use to construct the new credentials.
|
||||
audience (str): the `aud` claim. The intended audience for the
|
||||
credentials.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: A new Credentials instance.
|
||||
"""
|
||||
kwargs.setdefault('issuer', credentials.signer_email)
|
||||
kwargs.setdefault('subject', credentials.signer_email)
|
||||
return cls(
|
||||
credentials.signer,
|
||||
audience=audience,
|
||||
**kwargs)
|
||||
|
||||
def with_claims(self, issuer=None, subject=None, audience=None,
|
||||
additional_claims=None):
|
||||
"""Returns a copy of these credentials with modified claims.
|
||||
|
||||
Args:
|
||||
issuer (str): The `iss` claim. If unspecified the current issuer
|
||||
claim will be used.
|
||||
subject (str): The `sub` claim. If unspecified the current subject
|
||||
claim will be used.
|
||||
audience (str): the `aud` claim. If unspecified the current
|
||||
audience claim will be used.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT payload. This will be merged with the current
|
||||
additional claims.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: A new credentials instance.
|
||||
"""
|
||||
new_additional_claims = copy.deepcopy(self._additional_claims)
|
||||
new_additional_claims.update(additional_claims or {})
|
||||
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
issuer=issuer if issuer is not None else self._issuer,
|
||||
subject=subject if subject is not None else self._subject,
|
||||
audience=audience if audience is not None else self._audience,
|
||||
additional_claims=new_additional_claims)
|
||||
|
||||
def _make_jwt(self):
|
||||
"""Make a signed JWT.
|
||||
|
||||
Returns:
|
||||
Tuple[bytes, datetime]: The encoded JWT and the expiration.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=self._token_lifetime)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
'iss': self._issuer,
|
||||
'sub': self._subject,
|
||||
'iat': _helpers.datetime_to_secs(now),
|
||||
'exp': _helpers.datetime_to_secs(expiry),
|
||||
'aud': self._audience,
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
jwt = encode(self._signer, payload)
|
||||
|
||||
return jwt, expiry
|
||||
|
||||
def refresh(self, request):
|
||||
"""Refreshes the access token.
|
||||
|
||||
Args:
|
||||
request (Any): Unused.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
# (pylint doesn't correctly recognize overridden methods.)
|
||||
self.token, self.expiry = self._make_jwt()
|
||||
|
||||
@_helpers.copy_docstring(google.auth.credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(google.auth.credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self._issuer
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(google.auth.credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
|
||||
class OnDemandCredentials(
|
||||
google.auth.credentials.Signing,
|
||||
google.auth.credentials.Credentials):
|
||||
"""On-demand JWT credentials.
|
||||
|
||||
Like :class:`Credentials`, this class uses a JWT as the bearer token for
|
||||
authentication. However, this class does not require the audience at
|
||||
construction time. Instead, it will generate a new token on-demand for
|
||||
each request using the request URI as the audience. It caches tokens
|
||||
so that multiple requests to the same URI do not incur the overhead
|
||||
of generating a new token every time.
|
||||
|
||||
This behavior is especially useful for `gRPC`_ clients. A gRPC service may
|
||||
have multiple audience and gRPC clients may not know all of the audiences
|
||||
required for accessing a particular service. With these credentials,
|
||||
no knowledge of the audiences is required ahead of time.
|
||||
|
||||
.. _grpc: http://www.grpc.io/
|
||||
"""
|
||||
|
||||
def __init__(self, signer, issuer, subject,
|
||||
additional_claims=None,
|
||||
token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
|
||||
max_cache_size=_DEFAULT_MAX_CACHE_SIZE):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
issuer (str): The `iss` claim.
|
||||
subject (str): The `sub` claim.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT payload.
|
||||
token_lifetime (int): The amount of time in seconds for
|
||||
which the token is valid. Defaults to 1 hour.
|
||||
max_cache_size (int): The maximum number of JWT tokens to keep in
|
||||
cache. Tokens are cached using :class:`cachetools.LRUCache`.
|
||||
"""
|
||||
super(OnDemandCredentials, self).__init__()
|
||||
self._signer = signer
|
||||
self._issuer = issuer
|
||||
self._subject = subject
|
||||
self._token_lifetime = token_lifetime
|
||||
|
||||
if additional_claims is None:
|
||||
additional_claims = {}
|
||||
|
||||
self._additional_claims = additional_claims
|
||||
self._cache = cachetools.LRUCache(maxsize=max_cache_size)
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info, **kwargs):
|
||||
"""Creates an OnDemandCredentials instance from a signer and service
|
||||
account info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.OnDemandCredentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
kwargs.setdefault('subject', info['client_email'])
|
||||
kwargs.setdefault('issuer', info['client_email'])
|
||||
return cls(signer, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info, **kwargs):
|
||||
"""Creates an OnDemandCredentials instance from a dictionary.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.OnDemandCredentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info, require=['client_email'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename, **kwargs):
|
||||
"""Creates an OnDemandCredentials instance from a service account .json
|
||||
file in Google format.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account .json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.OnDemandCredentials: The constructed credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename, require=['client_email'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_signing_credentials(cls, credentials, **kwargs):
|
||||
"""Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
|
||||
from an existing :class:`google.auth.credentials.Signing` instance.
|
||||
|
||||
The new instance will use the same signer as the existing instance and
|
||||
will use the existing instance's signer email as the issuer and
|
||||
subject by default.
|
||||
|
||||
Example::
|
||||
|
||||
svc_creds = service_account.Credentials.from_service_account_file(
|
||||
'service_account.json')
|
||||
jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
|
||||
svc_creds)
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Signing): The credentials to
|
||||
use to construct the new credentials.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: A new Credentials instance.
|
||||
"""
|
||||
kwargs.setdefault('issuer', credentials.signer_email)
|
||||
kwargs.setdefault('subject', credentials.signer_email)
|
||||
return cls(credentials.signer, **kwargs)
|
||||
|
||||
def with_claims(self, issuer=None, subject=None, additional_claims=None):
|
||||
"""Returns a copy of these credentials with modified claims.
|
||||
|
||||
Args:
|
||||
issuer (str): The `iss` claim. If unspecified the current issuer
|
||||
claim will be used.
|
||||
subject (str): The `sub` claim. If unspecified the current subject
|
||||
claim will be used.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT payload. This will be merged with the current
|
||||
additional claims.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.OnDemandCredentials: A new credentials instance.
|
||||
"""
|
||||
new_additional_claims = copy.deepcopy(self._additional_claims)
|
||||
new_additional_claims.update(additional_claims or {})
|
||||
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
issuer=issuer if issuer is not None else self._issuer,
|
||||
subject=subject if subject is not None else self._subject,
|
||||
additional_claims=new_additional_claims,
|
||||
max_cache_size=self._cache.maxsize)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""Checks the validity of the credentials.
|
||||
|
||||
These credentials are always valid because it generates tokens on
|
||||
demand.
|
||||
"""
|
||||
return True
|
||||
|
||||
def _make_jwt_for_audience(self, audience):
|
||||
"""Make a new JWT for the given audience.
|
||||
|
||||
Args:
|
||||
audience (str): The intended audience.
|
||||
|
||||
Returns:
|
||||
Tuple[bytes, datetime]: The encoded JWT and the expiration.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=self._token_lifetime)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
'iss': self._issuer,
|
||||
'sub': self._subject,
|
||||
'iat': _helpers.datetime_to_secs(now),
|
||||
'exp': _helpers.datetime_to_secs(expiry),
|
||||
'aud': audience,
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
jwt = encode(self._signer, payload)
|
||||
|
||||
return jwt, expiry
|
||||
|
||||
def _get_jwt_for_audience(self, audience):
|
||||
"""Get a JWT For a given audience.
|
||||
|
||||
If there is already an existing, non-expired token in the cache for
|
||||
the audience, that token is used. Otherwise, a new token will be
|
||||
created.
|
||||
|
||||
Args:
|
||||
audience (str): The intended audience.
|
||||
|
||||
Returns:
|
||||
bytes: The encoded JWT.
|
||||
"""
|
||||
token, expiry = self._cache.get(audience, (None, None))
|
||||
|
||||
if token is None or expiry < _helpers.utcnow():
|
||||
token, expiry = self._make_jwt_for_audience(audience)
|
||||
self._cache[audience] = token, expiry
|
||||
|
||||
return token
|
||||
|
||||
def refresh(self, request):
|
||||
"""Raises an exception, these credentials can not be directly
|
||||
refreshed.
|
||||
|
||||
Args:
|
||||
request (Any): Unused.
|
||||
|
||||
Raises:
|
||||
google.auth.RefreshError
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
# (pylint doesn't correctly recognize overridden methods.)
|
||||
raise exceptions.RefreshError(
|
||||
'OnDemandCredentials can not be directly refreshed.')
|
||||
|
||||
def before_request(self, request, method, url, headers):
|
||||
"""Performs credential-specific before request logic.
|
||||
|
||||
Args:
|
||||
request (Any): Unused. JWT credentials do not need to make an
|
||||
HTTP request to refresh.
|
||||
method (str): The request's HTTP method.
|
||||
url (str): The request's URI. This is used as the audience claim
|
||||
when generating the JWT.
|
||||
headers (Mapping): The request's headers.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
# (pylint doesn't correctly recognize overridden methods.)
|
||||
parts = urllib.parse.urlsplit(url)
|
||||
# Strip query string and fragment
|
||||
audience = urllib.parse.urlunsplit(
|
||||
(parts.scheme, parts.netloc, parts.path, None, None))
|
||||
token = self._get_jwt_for_audience(audience)
|
||||
self.apply(headers, token=token)
|
||||
|
||||
@_helpers.copy_docstring(google.auth.credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(google.auth.credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self._issuer
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(google.auth.credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Transport - HTTP client library support.
|
||||
|
||||
:mod:`google.auth` is designed to work with various HTTP client libraries such
|
||||
as urllib3 and requests. In order to work across these libraries with different
|
||||
interfaces some abstraction is needed.
|
||||
|
||||
This module provides two interfaces that are implemented by transport adapters
|
||||
to support HTTP libraries. :class:`Request` defines the interface expected by
|
||||
:mod:`google.auth` to make requests. :class:`Response` defines the interface
|
||||
for the return value of :class:`Request`.
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
from six.moves import http_client
|
||||
|
||||
DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
|
||||
"""Sequence[int]: Which HTTP status code indicate that credentials should be
|
||||
refreshed and a request should be retried.
|
||||
"""
|
||||
|
||||
DEFAULT_MAX_REFRESH_ATTEMPTS = 2
|
||||
"""int: How many times to refresh the credentials and retry a request."""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Response(object):
|
||||
"""HTTP Response data."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def status(self):
|
||||
"""int: The HTTP status code."""
|
||||
raise NotImplementedError('status must be implemented.')
|
||||
|
||||
@abc.abstractproperty
|
||||
def headers(self):
|
||||
"""Mapping[str, str]: The HTTP response headers."""
|
||||
raise NotImplementedError('headers must be implemented.')
|
||||
|
||||
@abc.abstractproperty
|
||||
def data(self):
|
||||
"""bytes: The response body."""
|
||||
raise NotImplementedError('data must be implemented.')
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Request(object):
|
||||
"""Interface for a callable that makes HTTP requests.
|
||||
|
||||
Specific transport implementations should provide an implementation of
|
||||
this that adapts their specific request / response API.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __call__(self, url, method='GET', body=None, headers=None,
|
||||
timeout=None, **kwargs):
|
||||
"""Make an HTTP request.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
transport-specific default timeout will be used.
|
||||
kwargs: Additionally arguments passed on to the transport's
|
||||
request method.
|
||||
|
||||
Returns:
|
||||
Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
# pylint: disable=redundant-returns-doc, missing-raises-doc
|
||||
# (pylint doesn't play well with abstract docstrings.)
|
||||
raise NotImplementedError('__call__ must be implemented.')
|
||||
@@ -1,113 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Transport adapter for http.client, for internal use only."""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import six
|
||||
from six.moves import http_client
|
||||
from six.moves import urllib
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Response(transport.Response):
|
||||
"""http.client transport response adapter.
|
||||
|
||||
Args:
|
||||
response (http.client.HTTPResponse): The raw http client response.
|
||||
"""
|
||||
def __init__(self, response):
|
||||
self._status = response.status
|
||||
self._headers = {
|
||||
key.lower(): value for key, value in response.getheaders()}
|
||||
self._data = response.read()
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""http.client transport request adapter."""
|
||||
|
||||
def __call__(self, url, method='GET', body=None, headers=None,
|
||||
timeout=None, **kwargs):
|
||||
"""Make an HTTP request using http.client.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping): Request headers.
|
||||
timeout (Optional(int)): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
socket global default timeout will be used.
|
||||
kwargs: Additional arguments passed throught to the underlying
|
||||
:meth:`~http.client.HTTPConnection.request` method.
|
||||
|
||||
Returns:
|
||||
Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
# socket._GLOBAL_DEFAULT_TIMEOUT is the default in http.client.
|
||||
if timeout is None:
|
||||
timeout = socket._GLOBAL_DEFAULT_TIMEOUT
|
||||
|
||||
# http.client doesn't allow None as the headers argument.
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# http.client needs the host and path parts specified separately.
|
||||
parts = urllib.parse.urlsplit(url)
|
||||
path = urllib.parse.urlunsplit(
|
||||
('', '', parts.path, parts.query, parts.fragment))
|
||||
|
||||
if parts.scheme != 'http':
|
||||
raise exceptions.TransportError(
|
||||
'http.client transport only supports the http scheme, {}'
|
||||
'was specified'.format(parts.scheme))
|
||||
|
||||
connection = http_client.HTTPConnection(parts.netloc, timeout=timeout)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('Making request: %s %s', method, url)
|
||||
|
||||
connection.request(
|
||||
method, path, body=body, headers=headers, **kwargs)
|
||||
response = connection.getresponse()
|
||||
return Response(response)
|
||||
|
||||
except (http_client.HTTPException, socket.error) as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
finally:
|
||||
connection.close()
|
||||
@@ -1,135 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Authorization support for gRPC."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
try:
|
||||
import grpc
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
six.raise_from(
|
||||
ImportError(
|
||||
'gRPC is not installed, please install the grpcio package '
|
||||
'to use the gRPC transport.'
|
||||
),
|
||||
caught_exc,
|
||||
)
|
||||
|
||||
|
||||
class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
|
||||
"""A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
|
||||
request.
|
||||
|
||||
.. _gRPC AuthMetadataPlugin:
|
||||
http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to requests.
|
||||
request (google.auth.transport.Request): A HTTP transport request
|
||||
object used to refresh credentials as needed.
|
||||
"""
|
||||
def __init__(self, credentials, request):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
# pylint doesn't realize that the super method takes no arguments
|
||||
# because this class is the same name as the superclass.
|
||||
super(AuthMetadataPlugin, self).__init__()
|
||||
self._credentials = credentials
|
||||
self._request = request
|
||||
|
||||
def _get_authorization_headers(self, context):
|
||||
"""Gets the authorization headers for a request.
|
||||
|
||||
Returns:
|
||||
Sequence[Tuple[str, str]]: A list of request headers (key, value)
|
||||
to add to the request.
|
||||
"""
|
||||
headers = {}
|
||||
self._credentials.before_request(
|
||||
self._request,
|
||||
context.method_name,
|
||||
context.service_url,
|
||||
headers)
|
||||
|
||||
return list(six.iteritems(headers))
|
||||
|
||||
def __call__(self, context, callback):
|
||||
"""Passes authorization metadata into the given callback.
|
||||
|
||||
Args:
|
||||
context (grpc.AuthMetadataContext): The RPC context.
|
||||
callback (grpc.AuthMetadataPluginCallback): The callback that will
|
||||
be invoked to pass in the authorization metadata.
|
||||
"""
|
||||
callback(self._get_authorization_headers(context), None)
|
||||
|
||||
|
||||
def secure_authorized_channel(
|
||||
credentials, request, target, ssl_credentials=None, **kwargs):
|
||||
"""Creates a secure authorized gRPC channel.
|
||||
|
||||
This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
|
||||
channel can be used to create a stub that can make authorized requests.
|
||||
|
||||
Example::
|
||||
|
||||
import google.auth
|
||||
import google.auth.transport.grpc
|
||||
import google.auth.transport.requests
|
||||
from google.cloud.speech.v1 import cloud_speech_pb2
|
||||
|
||||
# Get credentials.
|
||||
credentials, _ = google.auth.default()
|
||||
|
||||
# Get an HTTP request function to refresh credentials.
|
||||
request = google.auth.transport.requests.Request()
|
||||
|
||||
# Create a channel.
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, 'speech.googleapis.com:443', request)
|
||||
|
||||
# Use the channel to create a stub.
|
||||
cloud_speech.create_Speech_stub(channel)
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to requests.
|
||||
request (google.auth.transport.Request): A HTTP transport request
|
||||
object used to refresh credentials as needed. Even though gRPC
|
||||
is a separate transport, there's no way to refresh the credentials
|
||||
without using a standard http transport.
|
||||
target (str): The host and port of the service.
|
||||
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
|
||||
credentials. This can be used to specify different certificates.
|
||||
kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
|
||||
|
||||
Returns:
|
||||
grpc.Channel: The created gRPC channel.
|
||||
"""
|
||||
# Create the metadata plugin for inserting the authorization header.
|
||||
metadata_plugin = AuthMetadataPlugin(credentials, request)
|
||||
|
||||
# Create a set of grpc.CallCredentials using the metadata plugin.
|
||||
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
|
||||
|
||||
if ssl_credentials is None:
|
||||
ssl_credentials = grpc.ssl_channel_credentials()
|
||||
|
||||
# Combine the ssl credentials and the authorization credentials.
|
||||
composite_credentials = grpc.composite_channel_credentials(
|
||||
ssl_credentials, google_auth_credentials)
|
||||
|
||||
return grpc.secure_channel(target, composite_credentials, **kwargs)
|
||||
@@ -1,226 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Transport adapter for Requests."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
import logging
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
import six
|
||||
six.raise_from(
|
||||
ImportError(
|
||||
'The requests library is not installed, please install the '
|
||||
'requests package to use the requests transport.'
|
||||
),
|
||||
caught_exc,
|
||||
)
|
||||
import requests.adapters # pylint: disable=ungrouped-imports
|
||||
import requests.exceptions # pylint: disable=ungrouped-imports
|
||||
import six # pylint: disable=ungrouped-imports
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Response(transport.Response):
|
||||
"""Requests transport response adapter.
|
||||
|
||||
Args:
|
||||
response (requests.Response): The raw Requests response.
|
||||
"""
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._response.status_code
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._response.headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._response.content
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""Requests request adapter.
|
||||
|
||||
This class is used internally for making requests using various transports
|
||||
in a consistent way. If you use :class:`AuthorizedSession` you do not need
|
||||
to construct or use this class directly.
|
||||
|
||||
This class can be useful if you want to manually refresh a
|
||||
:class:`~google.auth.credentials.Credentials` instance::
|
||||
|
||||
import google.auth.transport.requests
|
||||
import requests
|
||||
|
||||
request = google.auth.transport.requests.Request()
|
||||
|
||||
credentials.refresh(request)
|
||||
|
||||
Args:
|
||||
session (requests.Session): An instance :class:`requests.Session` used
|
||||
to make HTTP requests. If not specified, a session will be created.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
def __init__(self, session=None):
|
||||
if not session:
|
||||
session = requests.Session()
|
||||
|
||||
self.session = session
|
||||
|
||||
def __call__(self, url, method='GET', body=None, headers=None,
|
||||
timeout=None, **kwargs):
|
||||
"""Make an HTTP request using requests.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
requests default timeout will be used.
|
||||
kwargs: Additional arguments passed through to the underlying
|
||||
requests :meth:`~requests.Session.request` method.
|
||||
|
||||
Returns:
|
||||
google.auth.transport.Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
try:
|
||||
_LOGGER.debug('Making request: %s %s', method, url)
|
||||
response = self.session.request(
|
||||
method, url, data=body, headers=headers, timeout=timeout,
|
||||
**kwargs)
|
||||
return _Response(response)
|
||||
except requests.exceptions.RequestException as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
|
||||
class AuthorizedSession(requests.Session):
|
||||
"""A Requests Session class with credentials.
|
||||
|
||||
This class is used to perform requests to API endpoints that require
|
||||
authorization::
|
||||
|
||||
from google.auth.transport.requests import AuthorizedSession
|
||||
|
||||
authed_session = AuthorizedSession(credentials)
|
||||
|
||||
response = authed_session.request(
|
||||
'GET', 'https://www.googleapis.com/storage/v1/b')
|
||||
|
||||
The underlying :meth:`request` implementation handles adding the
|
||||
credentials' headers to the request and refreshing credentials as needed.
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to the request.
|
||||
refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
|
||||
that credentials should be refreshed and the request should be
|
||||
retried.
|
||||
max_refresh_attempts (int): The maximum number of times to attempt to
|
||||
refresh the credentials and retry the request.
|
||||
refresh_timeout (Optional[int]): The timeout value in seconds for
|
||||
credential refresh HTTP requests.
|
||||
kwargs: Additional arguments passed to the :class:`requests.Session`
|
||||
constructor.
|
||||
"""
|
||||
def __init__(self, credentials,
|
||||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
|
||||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
|
||||
refresh_timeout=None,
|
||||
**kwargs):
|
||||
super(AuthorizedSession, self).__init__(**kwargs)
|
||||
self.credentials = credentials
|
||||
self._refresh_status_codes = refresh_status_codes
|
||||
self._max_refresh_attempts = max_refresh_attempts
|
||||
self._refresh_timeout = refresh_timeout
|
||||
|
||||
auth_request_session = requests.Session()
|
||||
|
||||
# Using an adapter to make HTTP requests robust to network errors.
|
||||
# This adapter retrys HTTP requests when network errors occur
|
||||
# and the requests seems safely retryable.
|
||||
retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
|
||||
auth_request_session.mount("https://", retry_adapter)
|
||||
|
||||
# Request instance used by internal methods (for example,
|
||||
# credentials.refresh).
|
||||
# Do not pass `self` as the session here, as it can lead to infinite
|
||||
# recursion.
|
||||
self._auth_request = Request(auth_request_session)
|
||||
|
||||
def request(self, method, url, data=None, headers=None, **kwargs):
|
||||
"""Implementation of Requests' request."""
|
||||
# pylint: disable=arguments-differ
|
||||
# Requests has a ton of arguments to request, but only two
|
||||
# (method, url) are required. We pass through all of the other
|
||||
# arguments to super, so no need to exhaustively list them here.
|
||||
|
||||
# Use a kwarg for this instead of an attribute to maintain
|
||||
# thread-safety.
|
||||
_credential_refresh_attempt = kwargs.pop(
|
||||
'_credential_refresh_attempt', 0)
|
||||
|
||||
# Make a copy of the headers. They will be modified by the credentials
|
||||
# and we want to pass the original headers if we recurse.
|
||||
request_headers = headers.copy() if headers is not None else {}
|
||||
|
||||
self.credentials.before_request(
|
||||
self._auth_request, method, url, request_headers)
|
||||
|
||||
response = super(AuthorizedSession, self).request(
|
||||
method, url, data=data, headers=request_headers, **kwargs)
|
||||
|
||||
# If the response indicated that the credentials needed to be
|
||||
# refreshed, then refresh the credentials and re-attempt the
|
||||
# request.
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
if (response.status_code in self._refresh_status_codes
|
||||
and _credential_refresh_attempt < self._max_refresh_attempts):
|
||||
|
||||
_LOGGER.info(
|
||||
'Refreshing credentials due to a %s response. Attempt %s/%s.',
|
||||
response.status_code, _credential_refresh_attempt + 1,
|
||||
self._max_refresh_attempts)
|
||||
|
||||
auth_request_with_timeout = functools.partial(
|
||||
self._auth_request, timeout=self._refresh_timeout)
|
||||
self.credentials.refresh(auth_request_with_timeout)
|
||||
|
||||
# Recurse. Pass in the original headers, not our modified set.
|
||||
return self.request(
|
||||
method, url, data=data, headers=headers,
|
||||
_credential_refresh_attempt=_credential_refresh_attempt + 1,
|
||||
**kwargs)
|
||||
|
||||
return response
|
||||
@@ -1,266 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Transport adapter for urllib3."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
|
||||
# to verify HTTPS requests, and certifi is the recommended and most reliable
|
||||
# way to get a root certificate bundle. See
|
||||
# http://urllib3.readthedocs.io/en/latest/user-guide.html\
|
||||
# #certificate-verification
|
||||
# For more details.
|
||||
try:
|
||||
import certifi
|
||||
except ImportError: # pragma: NO COVER
|
||||
certifi = None
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
import six
|
||||
six.raise_from(
|
||||
ImportError(
|
||||
'The urllib3 library is not installed, please install the '
|
||||
'urllib3 package to use the urllib3 transport.'
|
||||
),
|
||||
caught_exc,
|
||||
)
|
||||
import six
|
||||
import urllib3.exceptions # pylint: disable=ungrouped-imports
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Response(transport.Response):
|
||||
"""urllib3 transport response adapter.
|
||||
|
||||
Args:
|
||||
response (urllib3.response.HTTPResponse): The raw urllib3 response.
|
||||
"""
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._response.status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._response.headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._response.data
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""urllib3 request adapter.
|
||||
|
||||
This class is used internally for making requests using various transports
|
||||
in a consistent way. If you use :class:`AuthorizedHttp` you do not need
|
||||
to construct or use this class directly.
|
||||
|
||||
This class can be useful if you want to manually refresh a
|
||||
:class:`~google.auth.credentials.Credentials` instance::
|
||||
|
||||
import google.auth.transport.urllib3
|
||||
import urllib3
|
||||
|
||||
http = urllib3.PoolManager()
|
||||
request = google.auth.transport.urllib3.Request(http)
|
||||
|
||||
credentials.refresh(request)
|
||||
|
||||
Args:
|
||||
http (urllib3.request.RequestMethods): An instance of any urllib3
|
||||
class that implements :class:`~urllib3.request.RequestMethods`,
|
||||
usually :class:`urllib3.PoolManager`.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
def __init__(self, http):
|
||||
self.http = http
|
||||
|
||||
def __call__(self, url, method='GET', body=None, headers=None,
|
||||
timeout=None, **kwargs):
|
||||
"""Make an HTTP request using urllib3.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
urllib3 default timeout will be used.
|
||||
kwargs: Additional arguments passed throught to the underlying
|
||||
urllib3 :meth:`urlopen` method.
|
||||
|
||||
Returns:
|
||||
google.auth.transport.Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
# urllib3 uses a sentinel default value for timeout, so only set it if
|
||||
# specified.
|
||||
if timeout is not None:
|
||||
kwargs['timeout'] = timeout
|
||||
|
||||
try:
|
||||
_LOGGER.debug('Making request: %s %s', method, url)
|
||||
response = self.http.request(
|
||||
method, url, body=body, headers=headers, **kwargs)
|
||||
return _Response(response)
|
||||
except urllib3.exceptions.HTTPError as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
|
||||
def _make_default_http():
|
||||
if certifi is not None:
|
||||
return urllib3.PoolManager(
|
||||
cert_reqs='CERT_REQUIRED',
|
||||
ca_certs=certifi.where())
|
||||
else:
|
||||
return urllib3.PoolManager()
|
||||
|
||||
|
||||
class AuthorizedHttp(urllib3.request.RequestMethods):
|
||||
"""A urllib3 HTTP class with credentials.
|
||||
|
||||
This class is used to perform requests to API endpoints that require
|
||||
authorization::
|
||||
|
||||
from google.auth.transport.urllib3 import AuthorizedHttp
|
||||
|
||||
authed_http = AuthorizedHttp(credentials)
|
||||
|
||||
response = authed_http.request(
|
||||
'GET', 'https://www.googleapis.com/storage/v1/b')
|
||||
|
||||
This class implements :class:`urllib3.request.RequestMethods` and can be
|
||||
used just like any other :class:`urllib3.PoolManager`.
|
||||
|
||||
The underlying :meth:`urlopen` implementation handles adding the
|
||||
credentials' headers to the request and refreshing credentials as needed.
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to the request.
|
||||
http (urllib3.PoolManager): The underlying HTTP object to
|
||||
use to make requests. If not specified, a
|
||||
:class:`urllib3.PoolManager` instance will be constructed with
|
||||
sane defaults.
|
||||
refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
|
||||
that credentials should be refreshed and the request should be
|
||||
retried.
|
||||
max_refresh_attempts (int): The maximum number of times to attempt to
|
||||
refresh the credentials and retry the request.
|
||||
"""
|
||||
def __init__(self, credentials, http=None,
|
||||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
|
||||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS):
|
||||
|
||||
if http is None:
|
||||
http = _make_default_http()
|
||||
|
||||
self.credentials = credentials
|
||||
self.http = http
|
||||
self._refresh_status_codes = refresh_status_codes
|
||||
self._max_refresh_attempts = max_refresh_attempts
|
||||
# Request instance used by internal methods (for example,
|
||||
# credentials.refresh).
|
||||
self._request = Request(self.http)
|
||||
|
||||
super(AuthorizedHttp, self).__init__()
|
||||
|
||||
def urlopen(self, method, url, body=None, headers=None, **kwargs):
|
||||
"""Implementation of urllib3's urlopen."""
|
||||
# pylint: disable=arguments-differ
|
||||
# We use kwargs to collect additional args that we don't need to
|
||||
# introspect here. However, we do explicitly collect the two
|
||||
# positional arguments.
|
||||
|
||||
# Use a kwarg for this instead of an attribute to maintain
|
||||
# thread-safety.
|
||||
_credential_refresh_attempt = kwargs.pop(
|
||||
'_credential_refresh_attempt', 0)
|
||||
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
# Make a copy of the headers. They will be modified by the credentials
|
||||
# and we want to pass the original headers if we recurse.
|
||||
request_headers = headers.copy()
|
||||
|
||||
self.credentials.before_request(
|
||||
self._request, method, url, request_headers)
|
||||
|
||||
response = self.http.urlopen(
|
||||
method, url, body=body, headers=request_headers, **kwargs)
|
||||
|
||||
# If the response indicated that the credentials needed to be
|
||||
# refreshed, then refresh the credentials and re-attempt the
|
||||
# request.
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
# The reason urllib3's retries aren't used is because they
|
||||
# don't allow you to modify the request headers. :/
|
||||
if (response.status in self._refresh_status_codes
|
||||
and _credential_refresh_attempt < self._max_refresh_attempts):
|
||||
|
||||
_LOGGER.info(
|
||||
'Refreshing credentials due to a %s response. Attempt %s/%s.',
|
||||
response.status, _credential_refresh_attempt + 1,
|
||||
self._max_refresh_attempts)
|
||||
|
||||
self.credentials.refresh(self._request)
|
||||
|
||||
# Recurse. Pass in the original headers, not our modified set.
|
||||
return self.urlopen(
|
||||
method, url, body=body, headers=headers,
|
||||
_credential_refresh_attempt=_credential_refresh_attempt + 1,
|
||||
**kwargs)
|
||||
|
||||
return response
|
||||
|
||||
# Proxy methods for compliance with the urllib3.PoolManager interface
|
||||
|
||||
def __enter__(self):
|
||||
"""Proxy to ``self.http``."""
|
||||
return self.http.__enter__()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Proxy to ``self.http``."""
|
||||
return self.http.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""Proxy to ``self.http``."""
|
||||
return self.http.headers
|
||||
|
||||
@headers.setter
|
||||
def headers(self, value):
|
||||
"""Proxy to ``self.http``."""
|
||||
self.http.headers = value
|
||||
@@ -1,15 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google OAuth 2.0 Library for Python."""
|
||||
@@ -1,249 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""OAuth 2.0 client.
|
||||
|
||||
This is a client for interacting with an OAuth 2.0 authorization server's
|
||||
token endpoint.
|
||||
|
||||
For more information about the token endpoint, see
|
||||
`Section 3.1 of rfc6749`_
|
||||
|
||||
.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import six
|
||||
from six.moves import http_client
|
||||
from six.moves import urllib
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
|
||||
_URLENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded'
|
||||
_JWT_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
||||
_REFRESH_GRANT_TYPE = 'refresh_token'
|
||||
|
||||
|
||||
def _handle_error_response(response_body):
|
||||
""""Translates an error response into an exception.
|
||||
|
||||
Args:
|
||||
response_body (str): The decoded response data.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError
|
||||
"""
|
||||
try:
|
||||
error_data = json.loads(response_body)
|
||||
error_details = '{}: {}'.format(
|
||||
error_data['error'],
|
||||
error_data.get('error_description'))
|
||||
# If no details could be extracted, use the response data.
|
||||
except (KeyError, ValueError):
|
||||
error_details = response_body
|
||||
|
||||
raise exceptions.RefreshError(
|
||||
error_details, response_body)
|
||||
|
||||
|
||||
def _parse_expiry(response_data):
|
||||
"""Parses the expiry field from a response into a datetime.
|
||||
|
||||
Args:
|
||||
response_data (Mapping): The JSON-parsed response data.
|
||||
|
||||
Returns:
|
||||
Optional[datetime]: The expiration or ``None`` if no expiration was
|
||||
specified.
|
||||
"""
|
||||
expires_in = response_data.get('expires_in', None)
|
||||
|
||||
if expires_in is not None:
|
||||
return _helpers.utcnow() + datetime.timedelta(
|
||||
seconds=expires_in)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _token_endpoint_request(request, token_uri, body):
|
||||
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
body (Mapping[str, str]): The parameters to send in the request body.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: The JSON-decoded response data.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
body = urllib.parse.urlencode(body)
|
||||
headers = {
|
||||
'content-type': _URLENCODED_CONTENT_TYPE,
|
||||
}
|
||||
|
||||
response = request(
|
||||
method='POST', url=token_uri, headers=headers, body=body)
|
||||
|
||||
response_body = response.data.decode('utf-8')
|
||||
|
||||
if response.status != http_client.OK:
|
||||
_handle_error_response(response_body)
|
||||
|
||||
response_data = json.loads(response_body)
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
def jwt_grant(request, token_uri, assertion):
|
||||
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants.
|
||||
|
||||
For more details, see `rfc7523 section 4`_.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
assertion (str): The OAuth 2.0 assertion.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
|
||||
expiration, and additional data returned by the token endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
|
||||
.. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
|
||||
"""
|
||||
body = {
|
||||
'assertion': assertion,
|
||||
'grant_type': _JWT_GRANT_TYPE,
|
||||
}
|
||||
|
||||
response_data = _token_endpoint_request(request, token_uri, body)
|
||||
|
||||
try:
|
||||
access_token = response_data['access_token']
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
'No access token in response.', response_data)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
expiry = _parse_expiry(response_data)
|
||||
|
||||
return access_token, expiry, response_data
|
||||
|
||||
|
||||
def id_token_jwt_grant(request, token_uri, assertion):
|
||||
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
|
||||
requests an OpenID Connect ID Token instead of an access token.
|
||||
|
||||
This is a variant on the standard JWT Profile that is currently unique
|
||||
to Google. This was added for the benefit of authenticating to services
|
||||
that require ID Tokens instead of access tokens or JWT bearer tokens.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorization server's token endpoint
|
||||
URI.
|
||||
assertion (str): JWT token signed by a service account. The token's
|
||||
payload must include a ``target_audience`` claim.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[datetime], Mapping[str, str]]:
|
||||
The (encoded) Open ID Connect ID Token, expiration, and additional
|
||||
data returned by the endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
body = {
|
||||
'assertion': assertion,
|
||||
'grant_type': _JWT_GRANT_TYPE,
|
||||
}
|
||||
|
||||
response_data = _token_endpoint_request(request, token_uri, body)
|
||||
|
||||
try:
|
||||
id_token = response_data['id_token']
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
'No ID token in response.', response_data)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
payload = jwt.decode(id_token, verify=False)
|
||||
expiry = datetime.datetime.utcfromtimestamp(payload['exp'])
|
||||
|
||||
return id_token, expiry, response_data
|
||||
|
||||
|
||||
def refresh_grant(request, token_uri, refresh_token, client_id, client_secret):
|
||||
"""Implements the OAuth 2.0 refresh token grant.
|
||||
|
||||
For more details, see `rfc678 section 6`_.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
refresh_token (str): The refresh token to use to get a new access
|
||||
token.
|
||||
client_id (str): The OAuth 2.0 application's client ID.
|
||||
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
|
||||
access token, new refresh token, expiration, and additional data
|
||||
returned by the token endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
|
||||
.. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
body = {
|
||||
'grant_type': _REFRESH_GRANT_TYPE,
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'refresh_token': refresh_token,
|
||||
}
|
||||
|
||||
response_data = _token_endpoint_request(request, token_uri, body)
|
||||
|
||||
try:
|
||||
access_token = response_data['access_token']
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
'No access token in response.', response_data)
|
||||
six.raise_from(new_exc, caught_exc)
|
||||
|
||||
refresh_token = response_data.get('refresh_token', refresh_token)
|
||||
expiry = _parse_expiry(response_data)
|
||||
|
||||
return access_token, refresh_token, expiry, response_data
|
||||
@@ -1,194 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""OAuth 2.0 Credentials.
|
||||
|
||||
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
||||
These credentials usually access resources on behalf of a user (resource
|
||||
owner).
|
||||
|
||||
Specifically, this is intended to use access tokens acquired using the
|
||||
`Authorization Code grant`_ and can refresh those tokens using a
|
||||
optional `refresh token`_.
|
||||
|
||||
Obtaining the initial access and refresh token is outside of the scope of this
|
||||
module. Consult `rfc6749 section 4.1`_ for complete details on the
|
||||
Authorization Code grant flow.
|
||||
|
||||
.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
|
||||
.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
|
||||
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import credentials
|
||||
from google.auth import exceptions
|
||||
from google.oauth2 import _client
|
||||
|
||||
|
||||
# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
|
||||
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/token'
|
||||
|
||||
|
||||
class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
|
||||
"""Credentials using OAuth 2.0 access and refresh tokens."""
|
||||
|
||||
def __init__(self, token, refresh_token=None, id_token=None,
|
||||
token_uri=None, client_id=None, client_secret=None,
|
||||
scopes=None):
|
||||
"""
|
||||
Args:
|
||||
token (Optional(str)): The OAuth 2.0 access token. Can be None
|
||||
if refresh information is provided.
|
||||
refresh_token (str): The OAuth 2.0 refresh token. If specified,
|
||||
credentials can be refreshed.
|
||||
id_token (str): The Open ID Connect ID Token.
|
||||
token_uri (str): The OAuth 2.0 authorization server's token
|
||||
endpoint URI. Must be specified for refresh, can be left as
|
||||
None if the token can not be refreshed.
|
||||
client_id (str): The OAuth 2.0 client ID. Must be specified for
|
||||
refresh, can be left as None if the token can not be refreshed.
|
||||
client_secret(str): The OAuth 2.0 client secret. Must be specified
|
||||
for refresh, can be left as None if the token can not be
|
||||
refreshed.
|
||||
scopes (Sequence[str]): The scopes that were originally used
|
||||
to obtain authorization. This is a purely informative parameter
|
||||
that can be used by :meth:`has_scopes`. OAuth 2.0 credentials
|
||||
can not request additional scopes after authorization.
|
||||
"""
|
||||
super(Credentials, self).__init__()
|
||||
self.token = token
|
||||
self._refresh_token = refresh_token
|
||||
self._id_token = id_token
|
||||
self._scopes = scopes
|
||||
self._token_uri = token_uri
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
|
||||
@property
|
||||
def refresh_token(self):
|
||||
"""Optional[str]: The OAuth 2.0 refresh token."""
|
||||
return self._refresh_token
|
||||
|
||||
@property
|
||||
def token_uri(self):
|
||||
"""Optional[str]: The OAuth 2.0 authorization server's token endpoint
|
||||
URI."""
|
||||
return self._token_uri
|
||||
|
||||
@property
|
||||
def id_token(self):
|
||||
"""Optional[str]: The Open ID Connect ID Token.
|
||||
|
||||
Depending on the authorization server and the scopes requested, this
|
||||
may be populated when credentials are obtained and updated when
|
||||
:meth:`refresh` is called. This token is a JWT. It can be verified
|
||||
and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
|
||||
"""
|
||||
return self._id_token
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
"""Optional[str]: The OAuth 2.0 client ID."""
|
||||
return self._client_id
|
||||
|
||||
@property
|
||||
def client_secret(self):
|
||||
"""Optional[str]: The OAuth 2.0 client secret."""
|
||||
return self._client_secret
|
||||
|
||||
@property
|
||||
def requires_scopes(self):
|
||||
"""False: OAuth 2.0 credentials have their scopes set when
|
||||
the initial token is requested and can not be changed."""
|
||||
return False
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
if (self._refresh_token is None or
|
||||
self._token_uri is None or
|
||||
self._client_id is None or
|
||||
self._client_secret is None):
|
||||
raise exceptions.RefreshError(
|
||||
'The credentials do not contain the necessary fields need to '
|
||||
'refresh the access token. You must specify refresh_token, '
|
||||
'token_uri, client_id, and client_secret.')
|
||||
|
||||
access_token, refresh_token, expiry, grant_response = (
|
||||
_client.refresh_grant(
|
||||
request, self._token_uri, self._refresh_token, self._client_id,
|
||||
self._client_secret))
|
||||
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
self._refresh_token = refresh_token
|
||||
self._id_token = grant_response.get('id_token')
|
||||
|
||||
@classmethod
|
||||
def from_authorized_user_info(cls, info, scopes=None):
|
||||
"""Creates a Credentials instance from parsed authorized user info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The authorized user info in Google
|
||||
format.
|
||||
scopes (Sequence[str]): Optional list of scopes to include in the
|
||||
credentials.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
keys_needed = set(('refresh_token', 'client_id', 'client_secret'))
|
||||
missing = keys_needed.difference(six.iterkeys(info))
|
||||
|
||||
if missing:
|
||||
raise ValueError(
|
||||
'Authorized user info was not in the expected format, missing '
|
||||
'fields {}.'.format(', '.join(missing)))
|
||||
|
||||
return Credentials(
|
||||
None, # No access token, must be refreshed.
|
||||
refresh_token=info['refresh_token'],
|
||||
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
||||
scopes=scopes,
|
||||
client_id=info['client_id'],
|
||||
client_secret=info['client_secret'])
|
||||
|
||||
@classmethod
|
||||
def from_authorized_user_file(cls, filename, scopes=None):
|
||||
"""Creates a Credentials instance from an authorized user json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the authorized user json file.
|
||||
scopes (Sequence[str]): Optional list of scopes to include in the
|
||||
credentials.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the file is not in the expected format.
|
||||
"""
|
||||
with io.open(filename, 'r', encoding='utf-8') as json_file:
|
||||
data = json.load(json_file)
|
||||
return cls.from_authorized_user_info(data, scopes)
|
||||
@@ -1,159 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google ID Token helpers.
|
||||
|
||||
Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
|
||||
generated by Google infrastructure.
|
||||
|
||||
To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
|
||||
server use :func:`verify_oauth2_token`. To verify an ID Token issued by
|
||||
Firebase, use :func:`verify_firebase_token`.
|
||||
|
||||
A general purpose ID Token verifier is available as :func:`verify_token`.
|
||||
|
||||
Example::
|
||||
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests
|
||||
|
||||
request = requests.Request()
|
||||
|
||||
id_info = id_token.verify_oauth2_token(
|
||||
token, request, 'my-client-id.example.com')
|
||||
|
||||
if id_info['iss'] != 'https://accounts.google.com':
|
||||
raise ValueError('Wrong issuer.')
|
||||
|
||||
userid = id_info['sub']
|
||||
|
||||
By default, this will re-fetch certificates for each verification. Because
|
||||
Google's public keys are only changed infrequently (on the order of once per
|
||||
day), you may wish to take advantage of caching to reduce latency and the
|
||||
potential for network errors. This can be accomplished using an external
|
||||
library like `CacheControl`_ to create a cache-aware
|
||||
:class:`google.auth.transport.Request`::
|
||||
|
||||
import cachecontrol
|
||||
import google.auth.transport.requests
|
||||
import requests
|
||||
|
||||
session = requests.session()
|
||||
cached_session = cachecontrol.CacheControl(session)
|
||||
request = google.auth.transport.requests.Request(session=cached_session)
|
||||
|
||||
.. _OpenID Connect ID Token:
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
.. _CacheControl: https://cachecontrol.readthedocs.io
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from six.moves import http_client
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
|
||||
# The URL that provides public certificates for verifying ID tokens issued
|
||||
# by Google's OAuth 2.0 authorization server.
|
||||
_GOOGLE_OAUTH2_CERTS_URL = 'https://www.googleapis.com/oauth2/v1/certs'
|
||||
|
||||
# The URL that provides public certificates for verifying ID tokens issued
|
||||
# by Firebase and the Google APIs infrastructure
|
||||
_GOOGLE_APIS_CERTS_URL = (
|
||||
'https://www.googleapis.com/robot/v1/metadata/x509'
|
||||
'/securetoken@system.gserviceaccount.com')
|
||||
|
||||
|
||||
def _fetch_certs(request, certs_url):
|
||||
"""Fetches certificates.
|
||||
|
||||
Google-style cerificate endpoints return JSON in the format of
|
||||
``{'key id': 'x509 certificate'}``.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
certs_url (str): The certificate endpoint URL.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: A mapping of public key ID to x.509 certificate
|
||||
data.
|
||||
"""
|
||||
response = request(certs_url, method='GET')
|
||||
|
||||
if response.status != http_client.OK:
|
||||
raise exceptions.TransportError(
|
||||
'Could not fetch certificates at {}'.format(certs_url))
|
||||
|
||||
return json.loads(response.data.decode('utf-8'))
|
||||
|
||||
|
||||
def verify_token(id_token, request, audience=None,
|
||||
certs_url=_GOOGLE_OAUTH2_CERTS_URL):
|
||||
"""Verifies an ID token and returns the decoded token.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this token is intended for. If None
|
||||
then the audience is not verified.
|
||||
certs_url (str): The URL that specifies the certificates to use to
|
||||
verify the token. This URL should return JSON in the format of
|
||||
``{'key id': 'x509 certificate'}``.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
certs = _fetch_certs(request, certs_url)
|
||||
|
||||
return jwt.decode(id_token, certs=certs, audience=audience)
|
||||
|
||||
|
||||
def verify_oauth2_token(id_token, request, audience=None):
|
||||
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this token is intended for. This is
|
||||
typically your application's OAuth 2.0 client ID. If None then the
|
||||
audience is not verified.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
return verify_token(
|
||||
id_token, request, audience=audience,
|
||||
certs_url=_GOOGLE_OAUTH2_CERTS_URL)
|
||||
|
||||
|
||||
def verify_firebase_token(id_token, request, audience=None):
|
||||
"""Verifies an ID Token issued by Firebase Authentication.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this token is intended for. This is
|
||||
typically your Firebase application ID. If None then the audience
|
||||
is not verified.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
return verify_token(
|
||||
id_token, request, audience=audience, certs_url=_GOOGLE_APIS_CERTS_URL)
|
||||
@@ -1,542 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
|
||||
|
||||
This module implements the JWT Profile for OAuth 2.0 Authorization Grants
|
||||
as defined by `RFC 7523`_ with particular support for how this RFC is
|
||||
implemented in Google's infrastructure. Google refers to these credentials
|
||||
as *Service Accounts*.
|
||||
|
||||
Service accounts are used for server-to-server communication, such as
|
||||
interactions between a web application server and a Google service. The
|
||||
service account belongs to your application instead of to an individual end
|
||||
user. In contrast to other OAuth 2.0 profiles, no users are involved and your
|
||||
application "acts" as the service account.
|
||||
|
||||
Typically an application uses a service account when the application uses
|
||||
Google APIs to work with its own data rather than a user's data. For example,
|
||||
an application that uses Google Cloud Datastore for data persistence would use
|
||||
a service account to authenticate its calls to the Google Cloud Datastore API.
|
||||
However, an application that needs to access a user's Drive documents would
|
||||
use the normal OAuth 2.0 profile.
|
||||
|
||||
Additionally, Google Apps domain administrators can grant service accounts
|
||||
`domain-wide delegation`_ authority to access user data on behalf of users in
|
||||
the domain.
|
||||
|
||||
This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
|
||||
in place of the usual authorization token returned during the standard
|
||||
OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
|
||||
the acquired access token is used as the bearer token when making requests
|
||||
using these credentials.
|
||||
|
||||
This profile differs from normal OAuth 2.0 profile because no user consent
|
||||
step is required. The use of the private key allows this profile to assert
|
||||
identity directly.
|
||||
|
||||
This profile also differs from the :mod:`google.auth.jwt` authentication
|
||||
because the JWT credentials use the JWT directly as the bearer token. This
|
||||
profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
|
||||
obtained OAuth 2.0 access token is used as the bearer token.
|
||||
|
||||
Domain-wide delegation
|
||||
----------------------
|
||||
|
||||
Domain-wide delegation allows a service account to access user data on
|
||||
behalf of any user in a Google Apps domain without consent from the user.
|
||||
For example, an application that uses the Google Calendar API to add events to
|
||||
the calendars of all users in a Google Apps domain would use a service account
|
||||
to access the Google Calendar API on behalf of users.
|
||||
|
||||
The Google Apps administrator must explicitly authorize the service account to
|
||||
do this. This authorization step is referred to as "delegating domain-wide
|
||||
authority" to a service account.
|
||||
|
||||
You can use domain-wise delegation by creating a set of credentials with a
|
||||
specific subject using :meth:`~Credentials.with_subject`.
|
||||
|
||||
.. _RFC 7523: https://tools.ietf.org/html/rfc7523
|
||||
"""
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import _service_account_info
|
||||
from google.auth import credentials
|
||||
from google.auth import jwt
|
||||
from google.oauth2 import _client
|
||||
|
||||
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
|
||||
|
||||
class Credentials(credentials.Signing,
|
||||
credentials.Scoped,
|
||||
credentials.Credentials):
|
||||
"""Service account credentials
|
||||
|
||||
Usually, you'll create these credentials with one of the helper
|
||||
constructors. To create credentials using a Google service account
|
||||
private key JSON file::
|
||||
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
'service-account.json')
|
||||
|
||||
Or if you already have the service account file loaded::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = service_account.Credentials.from_service_account_info(
|
||||
service_account_info)
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify additional scopes and a subject if necessary::
|
||||
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
scopes=['email'],
|
||||
subject='user@example.com')
|
||||
|
||||
The credentials are considered immutable. If you want to modify the scopes
|
||||
or the subject used for delegation, use :meth:`with_scopes` or
|
||||
:meth:`with_subject`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(['email'])
|
||||
delegated_credentials = credentials.with_subject(subject)
|
||||
"""
|
||||
|
||||
def __init__(self, signer, service_account_email, token_uri, scopes=None,
|
||||
subject=None, project_id=None, additional_claims=None):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
service_account_email (str): The service account's email.
|
||||
scopes (Sequence[str]): Scopes to request during the authorization
|
||||
grant.
|
||||
token_uri (str): The OAuth 2.0 Token URI.
|
||||
subject (str): For domain-wide delegation, the email address of the
|
||||
user to for which to request delegated access.
|
||||
project_id (str): Project ID associated with the service account
|
||||
credential.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT assertion used in the authorization grant.
|
||||
|
||||
.. note:: Typically one of the helper constructors
|
||||
:meth:`from_service_account_file` or
|
||||
:meth:`from_service_account_info` are used instead of calling the
|
||||
constructor directly.
|
||||
"""
|
||||
super(Credentials, self).__init__()
|
||||
|
||||
self._scopes = scopes
|
||||
self._signer = signer
|
||||
self._service_account_email = service_account_email
|
||||
self._subject = subject
|
||||
self._project_id = project_id
|
||||
self._token_uri = token_uri
|
||||
|
||||
if additional_claims is not None:
|
||||
self._additional_claims = additional_claims
|
||||
else:
|
||||
self._additional_claims = {}
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info, **kwargs):
|
||||
"""Creates a Credentials instance from a signer and service account
|
||||
info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
return cls(
|
||||
signer,
|
||||
service_account_email=info['client_email'],
|
||||
token_uri=info['token_uri'],
|
||||
project_id=info.get('project_id'), **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info, **kwargs):
|
||||
"""Creates a Credentials instance from parsed service account info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info, require=['client_email', 'token_uri'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename, **kwargs):
|
||||
"""Creates a Credentials instance from a service account json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: The constructed
|
||||
credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename, require=['client_email', 'token_uri'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email."""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
"""Project ID associated with this credential."""
|
||||
return self._project_id
|
||||
|
||||
@property
|
||||
def requires_scopes(self):
|
||||
"""Checks if the credentials requires scopes.
|
||||
|
||||
Returns:
|
||||
bool: True if there are no scopes set otherwise False.
|
||||
"""
|
||||
return True if not self._scopes else False
|
||||
|
||||
@_helpers.copy_docstring(credentials.Scoped)
|
||||
def with_scopes(self, scopes):
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
scopes=scopes,
|
||||
token_uri=self._token_uri,
|
||||
subject=self._subject,
|
||||
project_id=self._project_id,
|
||||
additional_claims=self._additional_claims.copy())
|
||||
|
||||
def with_subject(self, subject):
|
||||
"""Create a copy of these credentials with the specified subject.
|
||||
|
||||
Args:
|
||||
subject (str): The subject claim.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
scopes=self._scopes,
|
||||
token_uri=self._token_uri,
|
||||
subject=subject,
|
||||
project_id=self._project_id,
|
||||
additional_claims=self._additional_claims.copy())
|
||||
|
||||
def with_claims(self, additional_claims):
|
||||
"""Returns a copy of these credentials with modified claims.
|
||||
|
||||
Args:
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT payload. This will be merged with the current
|
||||
additional claims.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
new_additional_claims = copy.deepcopy(self._additional_claims)
|
||||
new_additional_claims.update(additional_claims or {})
|
||||
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
scopes=self._scopes,
|
||||
token_uri=self._token_uri,
|
||||
subject=self._subject,
|
||||
project_id=self._project_id,
|
||||
additional_claims=new_additional_claims)
|
||||
|
||||
def _make_authorization_grant_assertion(self):
|
||||
"""Create the OAuth 2.0 assertion.
|
||||
|
||||
This assertion is used during the OAuth 2.0 grant to acquire an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
bytes: The authorization grant assertion.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
'iat': _helpers.datetime_to_secs(now),
|
||||
'exp': _helpers.datetime_to_secs(expiry),
|
||||
# The issuer must be the service account email.
|
||||
'iss': self._service_account_email,
|
||||
# The audience must be the auth token endpoint's URI
|
||||
'aud': self._token_uri,
|
||||
'scope': _helpers.scopes_to_string(self._scopes or ())
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
# The subject can be a user email for domain-wide delegation.
|
||||
if self._subject:
|
||||
payload.setdefault('sub', self._subject)
|
||||
|
||||
token = jwt.encode(self._signer, payload)
|
||||
|
||||
return token
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = _client.jwt_grant(
|
||||
request, self._token_uri, assertion)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self._service_account_email
|
||||
|
||||
|
||||
class IDTokenCredentials(credentials.Signing, credentials.Credentials):
|
||||
"""Open ID Connect ID Token-based service account credentials.
|
||||
|
||||
These credentials are largely similar to :class:`.Credentials`, but instead
|
||||
of using an OAuth 2.0 Access Token as the bearer token, they use an Open
|
||||
ID Connect ID Token as the bearer token. These credentials are useful when
|
||||
communicating to services that require ID Tokens and can not accept access
|
||||
tokens.
|
||||
|
||||
Usually, you'll create these credentials with one of the helper
|
||||
constructors. To create credentials using a Google service account
|
||||
private key JSON file::
|
||||
|
||||
credentials = (
|
||||
service_account.IDTokenCredentials.from_service_account_file(
|
||||
'service-account.json'))
|
||||
|
||||
Or if you already have the service account file loaded::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = (
|
||||
service_account.IDTokenCredentials.from_service_account_info(
|
||||
service_account_info))
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify additional scopes and a subject if necessary::
|
||||
|
||||
credentials = (
|
||||
service_account.IDTokenCredentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
scopes=['email'],
|
||||
subject='user@example.com'))
|
||||
`
|
||||
The credentials are considered immutable. If you want to modify the scopes
|
||||
or the subject used for delegation, use :meth:`with_scopes` or
|
||||
:meth:`with_subject`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(['email'])
|
||||
delegated_credentials = credentials.with_subject(subject)
|
||||
|
||||
"""
|
||||
def __init__(self, signer, service_account_email, token_uri,
|
||||
target_audience, additional_claims=None):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
service_account_email (str): The service account's email.
|
||||
token_uri (str): The OAuth 2.0 Token URI.
|
||||
target_audience (str): The intended audience for these credentials,
|
||||
used when requesting the ID Token. The ID Token's ``aud`` claim
|
||||
will be set to this string.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT assertion used in the authorization grant.
|
||||
|
||||
.. note:: Typically one of the helper constructors
|
||||
:meth:`from_service_account_file` or
|
||||
:meth:`from_service_account_info` are used instead of calling the
|
||||
constructor directly.
|
||||
"""
|
||||
super(IDTokenCredentials, self).__init__()
|
||||
self._signer = signer
|
||||
self._service_account_email = service_account_email
|
||||
self._token_uri = token_uri
|
||||
self._target_audience = target_audience
|
||||
|
||||
if additional_claims is not None:
|
||||
self._additional_claims = additional_claims
|
||||
else:
|
||||
self._additional_claims = {}
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info, **kwargs):
|
||||
"""Creates a credentials instance from a signer and service account
|
||||
info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.IDTokenCredentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
kwargs.setdefault('service_account_email', info['client_email'])
|
||||
kwargs.setdefault('token_uri', info['token_uri'])
|
||||
return cls(signer, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info, **kwargs):
|
||||
"""Creates a credentials instance from parsed service account info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info, require=['client_email', 'token_uri'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename, **kwargs):
|
||||
"""Creates a credentials instance from a service account json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: The constructed
|
||||
credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename, require=['client_email', 'token_uri'])
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
def with_target_audience(self, target_audience):
|
||||
"""Create a copy of these credentials with the specified target
|
||||
audience.
|
||||
|
||||
Args:
|
||||
target_audience (str): The intended audience for these credentials,
|
||||
used when requesting the ID Token.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
token_uri=self._token_uri,
|
||||
target_audience=target_audience,
|
||||
additional_claims=self._additional_claims.copy())
|
||||
|
||||
def _make_authorization_grant_assertion(self):
|
||||
"""Create the OAuth 2.0 assertion.
|
||||
|
||||
This assertion is used during the OAuth 2.0 grant to acquire an
|
||||
ID token.
|
||||
|
||||
Returns:
|
||||
bytes: The authorization grant assertion.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
'iat': _helpers.datetime_to_secs(now),
|
||||
'exp': _helpers.datetime_to_secs(expiry),
|
||||
# The issuer must be the service account email.
|
||||
'iss': self.service_account_email,
|
||||
# The audience must be the auth token endpoint's URI
|
||||
'aud': self._token_uri,
|
||||
# The target audience specifies which service the ID token is
|
||||
# intended for.
|
||||
'target_audience': self._target_audience
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
token = jwt.encode(self._signer, payload)
|
||||
|
||||
return token
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = _client.id_token_jwt_grant(
|
||||
request, self._token_uri, assertion)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email."""
|
||||
return self._service_account_email
|
||||
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
@property
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self._service_account_email
|
||||
@@ -1,238 +0,0 @@
|
||||
# Copyright 2016 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Transport adapter for httplib2."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
import httplib2
|
||||
from six.moves import http_client
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Properties present in file-like streams / buffers.
|
||||
_STREAM_PROPERTIES = ('read', 'seek', 'tell')
|
||||
|
||||
|
||||
class _Response(transport.Response):
|
||||
"""httplib2 transport response adapter.
|
||||
|
||||
Args:
|
||||
response (httplib2.Response): The raw httplib2 response.
|
||||
data (bytes): The response body.
|
||||
"""
|
||||
def __init__(self, response, data):
|
||||
self._response = response
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""int: The HTTP status code."""
|
||||
return self._response.status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""Mapping[str, str]: The HTTP response headers."""
|
||||
return dict(self._response)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""bytes: The response body."""
|
||||
return self._data
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""httplib2 request adapter.
|
||||
|
||||
This class is used internally for making requests using various transports
|
||||
in a consistent way. If you use :class:`AuthorizedHttp` you do not need
|
||||
to construct or use this class directly.
|
||||
|
||||
This class can be useful if you want to manually refresh a
|
||||
:class:`~google.auth.credentials.Credentials` instance::
|
||||
|
||||
import google_auth_httplib2
|
||||
import httplib2
|
||||
|
||||
http = httplib2.Http()
|
||||
request = google_auth_httplib2.Request(http)
|
||||
|
||||
credentials.refresh(request)
|
||||
|
||||
Args:
|
||||
http (httplib2.Http): The underlying http object to use to make
|
||||
requests.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
def __init__(self, http):
|
||||
self.http = http
|
||||
|
||||
def __call__(self, url, method='GET', body=None, headers=None,
|
||||
timeout=None, **kwargs):
|
||||
"""Make an HTTP request using httplib2.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. This is ignored by httplib2 and will
|
||||
issue a warning.
|
||||
kwargs: Additional arguments passed throught to the underlying
|
||||
:meth:`httplib2.Http.request` method.
|
||||
|
||||
Returns:
|
||||
google.auth.transport.Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
if timeout is not None:
|
||||
_LOGGER.warning(
|
||||
'httplib2 transport does not support per-request timeout. '
|
||||
'Set the timeout when constructing the httplib2.Http instance.'
|
||||
)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('Making request: %s %s', method, url)
|
||||
response, data = self.http.request(
|
||||
url, method=method, body=body, headers=headers, **kwargs)
|
||||
return _Response(response, data)
|
||||
# httplib2 should catch the lower http error, this is a bug and
|
||||
# needs to be fixed there. Catch the error for the meanwhile.
|
||||
except (httplib2.HttpLib2Error, http_client.HTTPException) as exc:
|
||||
raise exceptions.TransportError(exc)
|
||||
|
||||
|
||||
def _make_default_http():
|
||||
"""Returns a default httplib2.Http instance."""
|
||||
return httplib2.Http()
|
||||
|
||||
|
||||
class AuthorizedHttp(object):
|
||||
"""A httplib2 HTTP class with credentials.
|
||||
|
||||
This class is used to perform requests to API endpoints that require
|
||||
authorization::
|
||||
|
||||
from google.auth.transport._httplib2 import AuthorizedHttp
|
||||
|
||||
authed_http = AuthorizedHttp(credentials)
|
||||
|
||||
response = authed_http.request(
|
||||
'https://www.googleapis.com/storage/v1/b')
|
||||
|
||||
This class implements :meth:`request` in the same way as
|
||||
:class:`httplib2.Http` and can usually be used just like any other
|
||||
instance of :class:``httplib2.Http`.
|
||||
|
||||
The underlying :meth:`request` implementation handles adding the
|
||||
credentials' headers to the request and refreshing credentials as needed.
|
||||
"""
|
||||
def __init__(self, credentials, http=None,
|
||||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
|
||||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS):
|
||||
"""
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials
|
||||
to add to the request.
|
||||
http (httplib2.Http): The underlying HTTP object to
|
||||
use to make requests. If not specified, a
|
||||
:class:`httplib2.Http` instance will be constructed.
|
||||
refresh_status_codes (Sequence[int]): Which HTTP status codes
|
||||
indicate that credentials should be refreshed and the request
|
||||
should be retried.
|
||||
max_refresh_attempts (int): The maximum number of times to attempt
|
||||
to refresh the credentials and retry the request.
|
||||
"""
|
||||
|
||||
if http is None:
|
||||
http = _make_default_http()
|
||||
|
||||
self.http = http
|
||||
self.credentials = credentials
|
||||
self._refresh_status_codes = refresh_status_codes
|
||||
self._max_refresh_attempts = max_refresh_attempts
|
||||
# Request instance used by internal methods (for example,
|
||||
# credentials.refresh).
|
||||
self._request = Request(self.http)
|
||||
|
||||
def request(self, uri, method='GET', body=None, headers=None,
|
||||
**kwargs):
|
||||
"""Implementation of httplib2's Http.request."""
|
||||
|
||||
_credential_refresh_attempt = kwargs.pop(
|
||||
'_credential_refresh_attempt', 0)
|
||||
|
||||
# Make a copy of the headers. They will be modified by the credentials
|
||||
# and we want to pass the original headers if we recurse.
|
||||
request_headers = headers.copy() if headers is not None else {}
|
||||
|
||||
self.credentials.before_request(
|
||||
self._request, method, uri, request_headers)
|
||||
|
||||
# Check if the body is a file-like stream, and if so, save the body
|
||||
# stream position so that it can be restored in case of refresh.
|
||||
body_stream_position = None
|
||||
if all(getattr(body, stream_prop, None) for stream_prop in
|
||||
_STREAM_PROPERTIES):
|
||||
body_stream_position = body.tell()
|
||||
|
||||
# Make the request.
|
||||
response, content = self.http.request(
|
||||
uri, method, body=body, headers=request_headers, **kwargs)
|
||||
|
||||
# If the response indicated that the credentials needed to be
|
||||
# refreshed, then refresh the credentials and re-attempt the
|
||||
# request.
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
if (response.status in self._refresh_status_codes
|
||||
and _credential_refresh_attempt < self._max_refresh_attempts):
|
||||
|
||||
_LOGGER.info(
|
||||
'Refreshing credentials due to a %s response. Attempt %s/%s.',
|
||||
response.status, _credential_refresh_attempt + 1,
|
||||
self._max_refresh_attempts)
|
||||
|
||||
self.credentials.refresh(self._request)
|
||||
|
||||
# Restore the body's stream position if needed.
|
||||
if body_stream_position is not None:
|
||||
body.seek(body_stream_position)
|
||||
|
||||
# Recurse. Pass in the original headers, not our modified set.
|
||||
return self.request(
|
||||
uri, method, body=body, headers=headers,
|
||||
_credential_refresh_attempt=_credential_refresh_attempt + 1,
|
||||
**kwargs)
|
||||
|
||||
return response, content
|
||||
|
||||
@property
|
||||
def connections(self):
|
||||
"""Proxy to httplib2.Http.connections."""
|
||||
return self.http.connections
|
||||
|
||||
@connections.setter
|
||||
def connections(self, value):
|
||||
"""Proxy to httplib2.Http.connections."""
|
||||
self.http.connections = value
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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__ = "1.7.3"
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
@@ -1,147 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helpers for authentication using oauth2client or google-auth."""
|
||||
|
||||
import httplib2
|
||||
|
||||
try:
|
||||
import google.auth
|
||||
import google.auth.credentials
|
||||
HAS_GOOGLE_AUTH = True
|
||||
except ImportError: # pragma: NO COVER
|
||||
HAS_GOOGLE_AUTH = False
|
||||
|
||||
try:
|
||||
import google_auth_httplib2
|
||||
except ImportError: # pragma: NO COVER
|
||||
google_auth_httplib2 = None
|
||||
|
||||
try:
|
||||
import oauth2client
|
||||
import oauth2client.client
|
||||
HAS_OAUTH2CLIENT = True
|
||||
except ImportError: # pragma: NO COVER
|
||||
HAS_OAUTH2CLIENT = False
|
||||
|
||||
|
||||
def default_credentials():
|
||||
"""Returns Application Default Credentials."""
|
||||
if HAS_GOOGLE_AUTH:
|
||||
credentials, _ = google.auth.default()
|
||||
return credentials
|
||||
elif HAS_OAUTH2CLIENT:
|
||||
return oauth2client.client.GoogleCredentials.get_application_default()
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
'No authentication library is available. Please install either '
|
||||
'google-auth or oauth2client.')
|
||||
|
||||
|
||||
def with_scopes(credentials, scopes):
|
||||
"""Scopes the credentials if necessary.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]): The credentials to scope.
|
||||
scopes (Sequence[str]): The list of scopes.
|
||||
|
||||
Returns:
|
||||
Union[google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]: The scoped credentials.
|
||||
"""
|
||||
if HAS_GOOGLE_AUTH and isinstance(
|
||||
credentials, google.auth.credentials.Credentials):
|
||||
return google.auth.credentials.with_scopes_if_required(
|
||||
credentials, scopes)
|
||||
else:
|
||||
try:
|
||||
if credentials.create_scoped_required():
|
||||
return credentials.create_scoped(scopes)
|
||||
else:
|
||||
return credentials
|
||||
except AttributeError:
|
||||
return credentials
|
||||
|
||||
|
||||
def authorized_http(credentials):
|
||||
"""Returns an http client that is authorized with the given credentials.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]): The credentials to use.
|
||||
|
||||
Returns:
|
||||
Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
|
||||
authorized http client.
|
||||
"""
|
||||
from googleapiclient.http import build_http
|
||||
|
||||
if HAS_GOOGLE_AUTH and isinstance(
|
||||
credentials, google.auth.credentials.Credentials):
|
||||
if google_auth_httplib2 is None:
|
||||
raise ValueError(
|
||||
'Credentials from google.auth specified, but '
|
||||
'google-api-python-client is unable to use these credentials '
|
||||
'unless google-auth-httplib2 is installed. Please install '
|
||||
'google-auth-httplib2.')
|
||||
return google_auth_httplib2.AuthorizedHttp(credentials,
|
||||
http=build_http())
|
||||
else:
|
||||
return credentials.authorize(build_http())
|
||||
|
||||
|
||||
def refresh_credentials(credentials):
|
||||
# Refresh must use a new http instance, as the one associated with the
|
||||
# credentials could be a AuthorizedHttp or an oauth2client-decorated
|
||||
# Http instance which would cause a weird recursive loop of refreshing
|
||||
# and likely tear a hole in spacetime.
|
||||
refresh_http = httplib2.Http()
|
||||
if HAS_GOOGLE_AUTH and isinstance(
|
||||
credentials, google.auth.credentials.Credentials):
|
||||
request = google_auth_httplib2.Request(refresh_http)
|
||||
return credentials.refresh(request)
|
||||
else:
|
||||
return credentials.refresh(refresh_http)
|
||||
|
||||
|
||||
def apply_credentials(credentials, headers):
|
||||
# oauth2client and google-auth have the same interface for this.
|
||||
if not is_valid(credentials):
|
||||
refresh_credentials(credentials)
|
||||
return credentials.apply(headers)
|
||||
|
||||
|
||||
def is_valid(credentials):
|
||||
if HAS_GOOGLE_AUTH and isinstance(
|
||||
credentials, google.auth.credentials.Credentials):
|
||||
return credentials.valid
|
||||
else:
|
||||
return (
|
||||
credentials.access_token is not None and
|
||||
not credentials.access_token_expired)
|
||||
|
||||
|
||||
def get_credentials_from_http(http):
|
||||
if http is None:
|
||||
return None
|
||||
elif hasattr(http.request, 'credentials'):
|
||||
return http.request.credentials
|
||||
elif (hasattr(http, 'credentials')
|
||||
and not isinstance(http.credentials, httplib2.Credentials)):
|
||||
return http.credentials
|
||||
else:
|
||||
return None
|
||||
@@ -1,204 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POSITIONAL_WARNING = 'WARNING'
|
||||
POSITIONAL_EXCEPTION = 'EXCEPTION'
|
||||
POSITIONAL_IGNORE = 'IGNORE'
|
||||
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
|
||||
POSITIONAL_IGNORE])
|
||||
|
||||
positional_parameters_enforcement = POSITIONAL_WARNING
|
||||
|
||||
_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
|
||||
_IS_DIR_MESSAGE = '{0}: Is a directory'
|
||||
_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
|
||||
|
||||
|
||||
def positional(max_positional_args):
|
||||
"""A decorator to declare that only the first N arguments my be positional.
|
||||
|
||||
This decorator makes it easy to support Python 3 style keyword-only
|
||||
parameters. For example, in Python 3 it is possible to write::
|
||||
|
||||
def fn(pos1, *, kwonly1=None, kwonly1=None):
|
||||
...
|
||||
|
||||
All named parameters after ``*`` must be a keyword::
|
||||
|
||||
fn(10, 'kw1', 'kw2') # Raises exception.
|
||||
fn(10, kwonly1='kw1') # Ok.
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
|
||||
To define a function like above, do::
|
||||
|
||||
@positional(1)
|
||||
def fn(pos1, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
If no default value is provided to a keyword argument, it becomes a
|
||||
required keyword argument::
|
||||
|
||||
@positional(0)
|
||||
def fn(required_kw):
|
||||
...
|
||||
|
||||
This must be called with the keyword parameter::
|
||||
|
||||
fn() # Raises exception.
|
||||
fn(10) # Raises exception.
|
||||
fn(required_kw=10) # Ok.
|
||||
|
||||
When defining instance or class methods always remember to account for
|
||||
``self`` and ``cls``::
|
||||
|
||||
class MyClass(object):
|
||||
|
||||
@positional(2)
|
||||
def my_method(self, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@positional(2)
|
||||
def my_method(cls, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
The positional decorator behavior is controlled by
|
||||
``_helpers.positional_parameters_enforcement``, which may be set to
|
||||
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
||||
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
||||
nothing, respectively, if a declaration is violated.
|
||||
|
||||
Args:
|
||||
max_positional_arguments: Maximum number of positional arguments. All
|
||||
parameters after the this index must be
|
||||
keyword only.
|
||||
|
||||
Returns:
|
||||
A decorator that prevents using arguments after max_positional_args
|
||||
from being used as positional parameters.
|
||||
|
||||
Raises:
|
||||
TypeError: if a key-word only argument is provided as a positional
|
||||
parameter, but only if
|
||||
_helpers.positional_parameters_enforcement is set to
|
||||
POSITIONAL_EXCEPTION.
|
||||
"""
|
||||
|
||||
def positional_decorator(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def positional_wrapper(*args, **kwargs):
|
||||
if len(args) > max_positional_args:
|
||||
plural_s = ''
|
||||
if max_positional_args != 1:
|
||||
plural_s = 's'
|
||||
message = ('{function}() takes at most {args_max} positional '
|
||||
'argument{plural} ({args_given} given)'.format(
|
||||
function=wrapped.__name__,
|
||||
args_max=max_positional_args,
|
||||
args_given=len(args),
|
||||
plural=plural_s))
|
||||
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
||||
raise TypeError(message)
|
||||
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
||||
logger.warning(message)
|
||||
return wrapped(*args, **kwargs)
|
||||
return positional_wrapper
|
||||
|
||||
if isinstance(max_positional_args, six.integer_types):
|
||||
return positional_decorator
|
||||
else:
|
||||
args, _, _, defaults = inspect.getargspec(max_positional_args)
|
||||
return positional(len(args) - len(defaults))(max_positional_args)
|
||||
|
||||
|
||||
def parse_unique_urlencoded(content):
|
||||
"""Parses unique key-value parameters from urlencoded content.
|
||||
|
||||
Args:
|
||||
content: string, URL-encoded key-value pairs.
|
||||
|
||||
Returns:
|
||||
dict, The key-value pairs from ``content``.
|
||||
|
||||
Raises:
|
||||
ValueError: if one of the keys is repeated.
|
||||
"""
|
||||
urlencoded_params = urllib.parse.parse_qs(content)
|
||||
params = {}
|
||||
for key, value in six.iteritems(urlencoded_params):
|
||||
if len(value) != 1:
|
||||
msg = ('URL-encoded content contains a repeated value:'
|
||||
'%s -> %s' % (key, ', '.join(value)))
|
||||
raise ValueError(msg)
|
||||
params[key] = value[0]
|
||||
return params
|
||||
|
||||
|
||||
def update_query_params(uri, params):
|
||||
"""Updates a URI with new query parameters.
|
||||
|
||||
If a given key from ``params`` is repeated in the ``uri``, then
|
||||
the URI will be considered invalid and an error will occur.
|
||||
|
||||
If the URI is valid, then each value from ``params`` will
|
||||
replace the corresponding value in the query parameters (if
|
||||
it exists).
|
||||
|
||||
Args:
|
||||
uri: string, A valid URI, with potential existing query parameters.
|
||||
params: dict, A dictionary of query parameters.
|
||||
|
||||
Returns:
|
||||
The same URI but with the new query parameters added.
|
||||
"""
|
||||
parts = urllib.parse.urlparse(uri)
|
||||
query_params = parse_unique_urlencoded(parts.query)
|
||||
query_params.update(params)
|
||||
new_query = urllib.parse.urlencode(query_params)
|
||||
new_parts = parts._replace(query=new_query)
|
||||
return urllib.parse.urlunparse(new_parts)
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
"""Adds a query parameter to a url.
|
||||
|
||||
Replaces the current value if it already exists in the URL.
|
||||
|
||||
Args:
|
||||
url: string, url to add the query parameter to.
|
||||
name: string, query parameter name.
|
||||
value: string, query parameter value.
|
||||
|
||||
Returns:
|
||||
Updated query parameter. Does not update the url if value is None.
|
||||
"""
|
||||
if value is None:
|
||||
return url
|
||||
else:
|
||||
return update_query_params(url, {name: value})
|
||||
@@ -1,287 +0,0 @@
|
||||
"""Channel notifications support.
|
||||
|
||||
Classes and functions to support channel subscriptions and notifications
|
||||
on those channels.
|
||||
|
||||
Notes:
|
||||
- This code is based on experimental APIs and is subject to change.
|
||||
- Notification does not do deduplication of notification ids, that's up to
|
||||
the receiver.
|
||||
- Storing the Channel between calls is up to the caller.
|
||||
|
||||
|
||||
Example setting up a channel:
|
||||
|
||||
# Create a new channel that gets notifications via webhook.
|
||||
channel = new_webhook_channel("https://example.com/my_web_hook")
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it before calling the
|
||||
# watch method because notifications may start arriving before the watch
|
||||
# method returns.
|
||||
...
|
||||
|
||||
resp = service.objects().watchAll(
|
||||
bucket="some_bucket_id", body=channel.body()).execute()
|
||||
channel.update(resp)
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it after being updated
|
||||
# since the resource_id value will now be correct, and that's needed to
|
||||
# stop a subscription.
|
||||
...
|
||||
|
||||
|
||||
An example Webhook implementation using webapp2. Note that webapp2 puts
|
||||
headers in a case insensitive dictionary, as headers aren't guaranteed to
|
||||
always be upper case.
|
||||
|
||||
id = self.request.headers[X_GOOG_CHANNEL_ID]
|
||||
|
||||
# Retrieve the channel by id.
|
||||
channel = ...
|
||||
|
||||
# Parse notification from the headers, including validating the id.
|
||||
n = notification_from_headers(channel, self.request.headers)
|
||||
|
||||
# Do app specific stuff with the notification here.
|
||||
if n.resource_state == 'sync':
|
||||
# Code to handle sync state.
|
||||
elif n.resource_state == 'exists':
|
||||
# Code to handle the exists state.
|
||||
elif n.resource_state == 'not_exists':
|
||||
# Code to handle the not exists state.
|
||||
|
||||
|
||||
Example of unsubscribing.
|
||||
|
||||
service.channels().stop(channel.body())
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from googleapiclient import errors
|
||||
from googleapiclient import _helpers as util
|
||||
import six
|
||||
|
||||
|
||||
# The unix time epoch starts at midnight 1970.
|
||||
EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||
|
||||
# Map the names of the parameters in the JSON channel description to
|
||||
# the parameter names we use in the Channel class.
|
||||
CHANNEL_PARAMS = {
|
||||
'address': 'address',
|
||||
'id': 'id',
|
||||
'expiration': 'expiration',
|
||||
'params': 'params',
|
||||
'resourceId': 'resource_id',
|
||||
'resourceUri': 'resource_uri',
|
||||
'type': 'type',
|
||||
'token': 'token',
|
||||
}
|
||||
|
||||
X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID'
|
||||
X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER'
|
||||
X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE'
|
||||
X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI'
|
||||
X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID'
|
||||
|
||||
|
||||
def _upper_header_keys(headers):
|
||||
new_headers = {}
|
||||
for k, v in six.iteritems(headers):
|
||||
new_headers[k.upper()] = v
|
||||
return new_headers
|
||||
|
||||
|
||||
class Notification(object):
|
||||
"""A Notification from a Channel.
|
||||
|
||||
Notifications are not usually constructed directly, but are returned
|
||||
from functions like notification_from_headers().
|
||||
|
||||
Attributes:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored.
|
||||
uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The unique identifier of the version of the resource at
|
||||
this event.
|
||||
"""
|
||||
@util.positional(5)
|
||||
def __init__(self, message_number, state, resource_uri, resource_id):
|
||||
"""Notification constructor.
|
||||
|
||||
Args:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored. Can be one
|
||||
of "exists", "not_exists", or "sync".
|
||||
resource_uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The identifier of the watched resource.
|
||||
"""
|
||||
self.message_number = message_number
|
||||
self.state = state
|
||||
self.resource_uri = resource_uri
|
||||
self.resource_id = resource_id
|
||||
|
||||
|
||||
class Channel(object):
|
||||
"""A Channel for notifications.
|
||||
|
||||
Usually not constructed directly, instead it is returned from helper
|
||||
functions like new_webhook_channel().
|
||||
|
||||
Attributes:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(self, type, id, token, address, expiration=None,
|
||||
params=None, resource_id="", resource_uri=""):
|
||||
"""Create a new Channel.
|
||||
|
||||
In user code, this Channel constructor will not typically be called
|
||||
manually since there are functions for creating channels for each specific
|
||||
type with a more customized set of arguments to pass.
|
||||
|
||||
Args:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.token = token
|
||||
self.address = address
|
||||
self.expiration = expiration
|
||||
self.params = params
|
||||
self.resource_id = resource_id
|
||||
self.resource_uri = resource_uri
|
||||
|
||||
def body(self):
|
||||
"""Build a body from the Channel.
|
||||
|
||||
Constructs a dictionary that's appropriate for passing into watch()
|
||||
methods as the value of body argument.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the channel.
|
||||
"""
|
||||
result = {
|
||||
'id': self.id,
|
||||
'token': self.token,
|
||||
'type': self.type,
|
||||
'address': self.address
|
||||
}
|
||||
if self.params:
|
||||
result['params'] = self.params
|
||||
if self.resource_id:
|
||||
result['resourceId'] = self.resource_id
|
||||
if self.resource_uri:
|
||||
result['resourceUri'] = self.resource_uri
|
||||
if self.expiration:
|
||||
result['expiration'] = self.expiration
|
||||
|
||||
return result
|
||||
|
||||
def update(self, resp):
|
||||
"""Update a channel with information from the response of watch().
|
||||
|
||||
When a request is sent to watch() a resource, the response returned
|
||||
from the watch() request is a dictionary with updated channel information,
|
||||
such as the resource_id, which is needed when stopping a subscription.
|
||||
|
||||
Args:
|
||||
resp: dict, The response from a watch() method.
|
||||
"""
|
||||
for json_name, param_name in six.iteritems(CHANNEL_PARAMS):
|
||||
value = resp.get(json_name)
|
||||
if value is not None:
|
||||
setattr(self, param_name, value)
|
||||
|
||||
|
||||
def notification_from_headers(channel, headers):
|
||||
"""Parse a notification from the webhook request headers, validate
|
||||
the notification, and return a Notification object.
|
||||
|
||||
Args:
|
||||
channel: Channel, The channel that the notification is associated with.
|
||||
headers: dict, A dictionary like object that contains the request headers
|
||||
from the webhook HTTP request.
|
||||
|
||||
Returns:
|
||||
A Notification object.
|
||||
|
||||
Raises:
|
||||
errors.InvalidNotificationError if the notification is invalid.
|
||||
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
|
||||
"""
|
||||
headers = _upper_header_keys(headers)
|
||||
channel_id = headers[X_GOOG_CHANNEL_ID]
|
||||
if channel.id != channel_id:
|
||||
raise errors.InvalidNotificationError(
|
||||
'Channel id mismatch: %s != %s' % (channel.id, channel_id))
|
||||
else:
|
||||
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
|
||||
state = headers[X_GOOG_RESOURCE_STATE]
|
||||
resource_uri = headers[X_GOOG_RESOURCE_URI]
|
||||
resource_id = headers[X_GOOG_RESOURCE_ID]
|
||||
return Notification(message_number, state, resource_uri, resource_id)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def new_webhook_channel(url, token=None, expiration=None, params=None):
|
||||
"""Create a new webhook Channel.
|
||||
|
||||
Args:
|
||||
url: str, URL to post notifications to.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each notification delivered
|
||||
over this channel.
|
||||
expiration: datetime.datetime, A time in the future when the channel
|
||||
should expire. Can also be None if the subscription should use the
|
||||
default expiration. Note that different services may have different
|
||||
limits on how long a subscription lasts. Check the response from the
|
||||
watch() method to see the value the service has set for an expiration
|
||||
time.
|
||||
params: dict, Extra parameters to pass on channel creation. Currently
|
||||
not used for webhook channels.
|
||||
"""
|
||||
expiration_ms = 0
|
||||
if expiration:
|
||||
delta = expiration - EPOCH
|
||||
expiration_ms = delta.microseconds/1000 + (
|
||||
delta.seconds + delta.days*24*3600)*1000
|
||||
if expiration_ms < 0:
|
||||
expiration_ms = 0
|
||||
|
||||
return Channel('web_hook', str(uuid.uuid4()),
|
||||
token, url, expiration=expiration_ms,
|
||||
params=params)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Caching utility for the discovery document."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
|
||||
|
||||
|
||||
def autodetect():
|
||||
"""Detects an appropriate cache module and returns it.
|
||||
|
||||
Returns:
|
||||
googleapiclient.discovery_cache.base.Cache, a cache object which
|
||||
is auto detected, or None if no cache object is available.
|
||||
"""
|
||||
try:
|
||||
from google.appengine.api import memcache
|
||||
from . import appengine_memcache
|
||||
return appengine_memcache.cache
|
||||
except Exception:
|
||||
try:
|
||||
from . import file_cache
|
||||
return file_cache.cache
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
return None
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""App Engine memcache based cache for the discovery document."""
|
||||
|
||||
import logging
|
||||
|
||||
# This is only an optional dependency because we only import this
|
||||
# module when google.appengine.api.memcache is available.
|
||||
from google.appengine.api import memcache
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NAMESPACE = 'google-api-client'
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A cache with app engine memcache API."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
|
||||
def get(self, url):
|
||||
try:
|
||||
return memcache.get(url, namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
def set(self, url, content):
|
||||
try:
|
||||
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""An abstract class for caching the discovery document."""
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""A base abstract cache class."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, url):
|
||||
"""Gets the content from the memcache with a given key.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
|
||||
Returns:
|
||||
object, the value in the cache for the given key, or None if the key is
|
||||
not in the cache.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(self, url, content):
|
||||
"""Sets the given key and content in the cache.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
content: string, the discovery document.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -1,141 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""File based cache for the discovery document.
|
||||
|
||||
The cache is stored in a single file so that multiple processes can
|
||||
share the same cache. It locks the file whenever accesing to the
|
||||
file. When the cache content is corrupted, it will be initialized with
|
||||
an empty cache.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
try:
|
||||
from oauth2client.contrib.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client < 2.0.0
|
||||
try:
|
||||
from oauth2client.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client > 4.0.0 or google-auth
|
||||
raise ImportError(
|
||||
'file_cache is unavailable when using oauth2client >= 4.0.0 or google-auth')
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FILENAME = 'google-api-python-client-discovery-doc.cache'
|
||||
EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||
|
||||
|
||||
def _to_timestamp(date):
|
||||
try:
|
||||
return (date - EPOCH).total_seconds()
|
||||
except AttributeError:
|
||||
# The following is the equivalent of total_seconds() in Python2.6.
|
||||
# See also: https://docs.python.org/2/library/datetime.html
|
||||
delta = date - EPOCH
|
||||
return ((delta.microseconds + (delta.seconds + delta.days * 24 * 3600)
|
||||
* 10**6) / 10**6)
|
||||
|
||||
|
||||
def _read_or_initialize_cache(f):
|
||||
f.file_handle().seek(0)
|
||||
try:
|
||||
cache = json.load(f.file_handle())
|
||||
except Exception:
|
||||
# This means it opens the file for the first time, or the cache is
|
||||
# corrupted, so initializing the file with an empty dict.
|
||||
cache = {}
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
return cache
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A file based cache for the discovery documents."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
self._file = os.path.join(tempfile.gettempdir(), FILENAME)
|
||||
f = LockedFile(self._file, 'a+', 'r')
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
_read_or_initialize_cache(f)
|
||||
# If we can not obtain the lock, other process or thread must
|
||||
# have initialized the file.
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def get(self, url):
|
||||
f = LockedFile(self._file, 'r+', 'r')
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
if url in cache:
|
||||
content, t = cache.get(url, (None, 0))
|
||||
if _to_timestamp(datetime.datetime.now()) < t + self._max_age:
|
||||
return content
|
||||
return None
|
||||
else:
|
||||
LOGGER.debug('Could not obtain a lock for the cache file.')
|
||||
return None
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def set(self, url, content):
|
||||
f = LockedFile(self._file, 'r+', 'r')
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
cache[url] = (content, _to_timestamp(datetime.datetime.now()))
|
||||
# Remove stale cache.
|
||||
for k, (_, timestamp) in list(cache.items()):
|
||||
if _to_timestamp(datetime.datetime.now()) >= timestamp + self._max_age:
|
||||
del cache[k]
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
else:
|
||||
LOGGER.debug('Could not obtain a lock for the cache file.')
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
@@ -1,157 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Errors for the library.
|
||||
|
||||
All exceptions defined by the library
|
||||
should be defined in this file.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import json
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class HttpError(Error):
|
||||
"""HTTP data was invalid or unexpected."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, resp, content, uri=None):
|
||||
self.resp = resp
|
||||
if not isinstance(content, bytes):
|
||||
raise TypeError("HTTP content should be bytes")
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
self.error_details = ''
|
||||
|
||||
def _get_reason(self):
|
||||
"""Calculate the reason for the error from the response content."""
|
||||
reason = self.resp.reason
|
||||
try:
|
||||
data = json.loads(self.content.decode('utf-8'))
|
||||
if isinstance(data, dict):
|
||||
reason = data['error']['message']
|
||||
if 'details' in data['error']:
|
||||
self.error_details = data['error']['details']
|
||||
elif isinstance(data, list) and len(data) > 0:
|
||||
first_error = data[0]
|
||||
reason = first_error['error']['message']
|
||||
if 'details' in first_error['error']:
|
||||
self.error_details = first_error['error']['details']
|
||||
except (ValueError, KeyError, TypeError):
|
||||
pass
|
||||
if reason is None:
|
||||
reason = ''
|
||||
return reason
|
||||
|
||||
def __repr__(self):
|
||||
reason = self._get_reason()
|
||||
if self.error_details:
|
||||
return '<HttpError %s when requesting %s returned "%s". Details: "%s">' % \
|
||||
(self.resp.status, self.uri, reason.strip(), self.error_details)
|
||||
elif self.uri:
|
||||
return '<HttpError %s when requesting %s returned "%s">' % (
|
||||
self.resp.status, self.uri, self._get_reason().strip())
|
||||
else:
|
||||
return '<HttpError %s "%s">' % (self.resp.status, self._get_reason())
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class InvalidJsonError(Error):
|
||||
"""The JSON returned could not be parsed."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownFileType(Error):
|
||||
"""File type unknown or unexpected."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownLinkType(Error):
|
||||
"""Link type unknown or unexpected."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownApiNameOrVersion(Error):
|
||||
"""No API with that name and version exists."""
|
||||
pass
|
||||
|
||||
|
||||
class UnacceptableMimeTypeError(Error):
|
||||
"""That is an unacceptable mimetype for this operation."""
|
||||
pass
|
||||
|
||||
|
||||
class MediaUploadSizeError(Error):
|
||||
"""Media is larger than the method can accept."""
|
||||
pass
|
||||
|
||||
|
||||
class ResumableUploadError(HttpError):
|
||||
"""Error occured during resumable upload."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChunkSizeError(Error):
|
||||
"""The given chunksize is not valid."""
|
||||
pass
|
||||
|
||||
class InvalidNotificationError(Error):
|
||||
"""The channel Notification is invalid."""
|
||||
pass
|
||||
|
||||
class BatchError(HttpError):
|
||||
"""Error occured during batch operations."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, reason, resp=None, content=None):
|
||||
self.resp = resp
|
||||
self.content = content
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
if getattr(self.resp, 'status', None) is None:
|
||||
return '<BatchError "%s">' % (self.reason)
|
||||
else:
|
||||
return '<BatchError %s "%s">' % (self.resp.status, self.reason)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class UnexpectedMethodError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected calls."""
|
||||
|
||||
@util.positional(1)
|
||||
def __init__(self, methodId=None):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedMethodError, self).__init__(
|
||||
'Received unexpected call %s' % methodId)
|
||||
|
||||
|
||||
class UnexpectedBodyError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected bodies."""
|
||||
|
||||
def __init__(self, expected, provided):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedBodyError, self).__init__(
|
||||
'Expected: [%s] - Provided: [%s]' % (expected, provided))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,175 +0,0 @@
|
||||
# Copyright 2014 Joe Gregorio
|
||||
#
|
||||
# Licensed under the MIT License
|
||||
|
||||
"""MIME-Type Parser
|
||||
|
||||
This module provides basic functions for handling mime-types. It can handle
|
||||
matching mime-types against a list of media-ranges. See section 14.1 of the
|
||||
HTTP specification [RFC 2616] for a complete explanation.
|
||||
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
||||
|
||||
Contents:
|
||||
- parse_mime_type(): Parses a mime-type into its component parts.
|
||||
- parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q'
|
||||
quality parameter.
|
||||
- quality(): Determines the quality ('q') of a mime-type when
|
||||
compared against a list of media-ranges.
|
||||
- quality_parsed(): Just like quality() except the second parameter must be
|
||||
pre-parsed.
|
||||
- best_match(): Choose the mime-type with the highest quality ('q')
|
||||
from a list of candidates.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from functools import reduce
|
||||
import six
|
||||
|
||||
__version__ = '0.1.3'
|
||||
__author__ = 'Joe Gregorio'
|
||||
__email__ = 'joe@bitworking.org'
|
||||
__license__ = 'MIT License'
|
||||
__credits__ = ''
|
||||
|
||||
|
||||
def parse_mime_type(mime_type):
|
||||
"""Parses a mime-type into its component parts.
|
||||
|
||||
Carves up a mime-type and returns a tuple of the (type, subtype, params)
|
||||
where 'params' is a dictionary of all the parameters for the media range.
|
||||
For example, the media range 'application/xhtml;q=0.5' would get parsed
|
||||
into:
|
||||
|
||||
('application', 'xhtml', {'q', '0.5'})
|
||||
"""
|
||||
parts = mime_type.split(';')
|
||||
params = dict([tuple([s.strip() for s in param.split('=', 1)])\
|
||||
for param in parts[1:]
|
||||
])
|
||||
full_type = parts[0].strip()
|
||||
# Java URLConnection class sends an Accept header that includes a
|
||||
# single '*'. Turn it into a legal wildcard.
|
||||
if full_type == '*':
|
||||
full_type = '*/*'
|
||||
(type, subtype) = full_type.split('/')
|
||||
|
||||
return (type.strip(), subtype.strip(), params)
|
||||
|
||||
|
||||
def parse_media_range(range):
|
||||
"""Parse a media-range into its component parts.
|
||||
|
||||
Carves up a media range and returns a tuple of the (type, subtype,
|
||||
params) where 'params' is a dictionary of all the parameters for the media
|
||||
range. For example, the media range 'application/*;q=0.5' would get parsed
|
||||
into:
|
||||
|
||||
('application', '*', {'q', '0.5'})
|
||||
|
||||
In addition this function also guarantees that there is a value for 'q'
|
||||
in the params dictionary, filling it in with a proper default if
|
||||
necessary.
|
||||
"""
|
||||
(type, subtype, params) = parse_mime_type(range)
|
||||
if 'q' not in params or not params['q'] or \
|
||||
not float(params['q']) or float(params['q']) > 1\
|
||||
or float(params['q']) < 0:
|
||||
params['q'] = '1'
|
||||
|
||||
return (type, subtype, params)
|
||||
|
||||
|
||||
def fitness_and_quality_parsed(mime_type, parsed_ranges):
|
||||
"""Find the best match for a mime-type amongst parsed media-ranges.
|
||||
|
||||
Find the best match for a given mime-type against a list of media_ranges
|
||||
that have already been parsed by parse_media_range(). Returns a tuple of
|
||||
the fitness value and the value of the 'q' quality parameter of the best
|
||||
match, or (-1, 0) if no match was found. Just as for quality_parsed(),
|
||||
'parsed_ranges' must be a list of parsed media ranges.
|
||||
"""
|
||||
best_fitness = -1
|
||||
best_fit_q = 0
|
||||
(target_type, target_subtype, target_params) =\
|
||||
parse_media_range(mime_type)
|
||||
for (type, subtype, params) in parsed_ranges:
|
||||
type_match = (type == target_type or\
|
||||
type == '*' or\
|
||||
target_type == '*')
|
||||
subtype_match = (subtype == target_subtype or\
|
||||
subtype == '*' or\
|
||||
target_subtype == '*')
|
||||
if type_match and subtype_match:
|
||||
param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \
|
||||
six.iteritems(target_params) if key != 'q' and \
|
||||
key in params and value == params[key]], 0)
|
||||
fitness = (type == target_type) and 100 or 0
|
||||
fitness += (subtype == target_subtype) and 10 or 0
|
||||
fitness += param_matches
|
||||
if fitness > best_fitness:
|
||||
best_fitness = fitness
|
||||
best_fit_q = params['q']
|
||||
|
||||
return best_fitness, float(best_fit_q)
|
||||
|
||||
|
||||
def quality_parsed(mime_type, parsed_ranges):
|
||||
"""Find the best match for a mime-type amongst parsed media-ranges.
|
||||
|
||||
Find the best match for a given mime-type against a list of media_ranges
|
||||
that have already been parsed by parse_media_range(). Returns the 'q'
|
||||
quality parameter of the best match, 0 if no match was found. This function
|
||||
bahaves the same as quality() except that 'parsed_ranges' must be a list of
|
||||
parsed media ranges.
|
||||
"""
|
||||
|
||||
return fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
|
||||
|
||||
|
||||
def quality(mime_type, ranges):
|
||||
"""Return the quality ('q') of a mime-type against a list of media-ranges.
|
||||
|
||||
Returns the quality 'q' of a mime-type when compared against the
|
||||
media-ranges in ranges. For example:
|
||||
|
||||
>>> quality('text/html','text/*;q=0.3, text/html;q=0.7,
|
||||
text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
|
||||
0.7
|
||||
|
||||
"""
|
||||
parsed_ranges = [parse_media_range(r) for r in ranges.split(',')]
|
||||
|
||||
return quality_parsed(mime_type, parsed_ranges)
|
||||
|
||||
|
||||
def best_match(supported, header):
|
||||
"""Return mime-type with the highest quality ('q') from list of candidates.
|
||||
|
||||
Takes a list of supported mime-types and finds the best match for all the
|
||||
media-ranges listed in header. The value of header must be a string that
|
||||
conforms to the format of the HTTP Accept: header. The value of 'supported'
|
||||
is a list of mime-types. The list of supported mime-types should be sorted
|
||||
in order of increasing desirability, in case of a situation where there is
|
||||
a tie.
|
||||
|
||||
>>> best_match(['application/xbel+xml', 'text/xml'],
|
||||
'text/*;q=0.5,*/*; q=0.1')
|
||||
'text/xml'
|
||||
"""
|
||||
split_header = _filter_blank(header.split(','))
|
||||
parsed_header = [parse_media_range(r) for r in split_header]
|
||||
weighted_matches = []
|
||||
pos = 0
|
||||
for mime_type in supported:
|
||||
weighted_matches.append((fitness_and_quality_parsed(mime_type,
|
||||
parsed_header), pos, mime_type))
|
||||
pos += 1
|
||||
weighted_matches.sort()
|
||||
|
||||
return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ''
|
||||
|
||||
|
||||
def _filter_blank(i):
|
||||
for s in i:
|
||||
if s.strip():
|
||||
yield s
|
||||
@@ -1,389 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Model objects for requests and responses.
|
||||
|
||||
Each API may support one or more serializations, such
|
||||
as JSON, Atom, etc. The model classes are responsible
|
||||
for converting between the wire format and the Python
|
||||
object representation.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
import six
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from googleapiclient import __version__
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
dump_request_response = False
|
||||
|
||||
|
||||
def _abstract():
|
||||
raise NotImplementedError('You need to override this function')
|
||||
|
||||
|
||||
class Model(object):
|
||||
"""Model base class.
|
||||
|
||||
All Model classes should implement this interface.
|
||||
The Model serializes and de-serializes between a wire
|
||||
format such as JSON and a Python object representation.
|
||||
"""
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized in the desired wire format.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
"""Base model class.
|
||||
|
||||
Subclasses should provide implementations for the "serialize" and
|
||||
"deserialize" methods, as well as values for the following class attributes.
|
||||
|
||||
Attributes:
|
||||
accept: The value to use for the HTTP Accept header.
|
||||
content_type: The value to use for the HTTP Content-type header.
|
||||
no_content_response: The value to return when deserializing a 204 "No
|
||||
Content" response.
|
||||
alt_param: The value to supply as the "alt" query parameter for requests.
|
||||
"""
|
||||
|
||||
accept = None
|
||||
content_type = None
|
||||
no_content_response = None
|
||||
alt_param = None
|
||||
|
||||
def _log_request(self, headers, path_params, query, body):
|
||||
"""Logs debugging information about the request if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info('--request-start--')
|
||||
LOGGER.info('-headers-start-')
|
||||
for h, v in six.iteritems(headers):
|
||||
LOGGER.info('%s: %s', h, v)
|
||||
LOGGER.info('-headers-end-')
|
||||
LOGGER.info('-path-parameters-start-')
|
||||
for h, v in six.iteritems(path_params):
|
||||
LOGGER.info('%s: %s', h, v)
|
||||
LOGGER.info('-path-parameters-end-')
|
||||
LOGGER.info('body: %s', body)
|
||||
LOGGER.info('query: %s', query)
|
||||
LOGGER.info('--request-end--')
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable by json.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized as JSON
|
||||
"""
|
||||
query = self._build_query(query_params)
|
||||
headers['accept'] = self.accept
|
||||
headers['accept-encoding'] = 'gzip, deflate'
|
||||
if 'user-agent' in headers:
|
||||
headers['user-agent'] += ' '
|
||||
else:
|
||||
headers['user-agent'] = ''
|
||||
headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__
|
||||
|
||||
if body_value is not None:
|
||||
headers['content-type'] = self.content_type
|
||||
body_value = self.serialize(body_value)
|
||||
self._log_request(headers, path_params, query, body_value)
|
||||
return (headers, path_params, query, body_value)
|
||||
|
||||
def _build_query(self, params):
|
||||
"""Builds a query string.
|
||||
|
||||
Args:
|
||||
params: dict, the query parameters
|
||||
|
||||
Returns:
|
||||
The query parameters properly encoded into an HTTP URI query string.
|
||||
"""
|
||||
if self.alt_param is not None:
|
||||
params.update({'alt': self.alt_param})
|
||||
astuples = []
|
||||
for key, value in six.iteritems(params):
|
||||
if type(value) == type([]):
|
||||
for x in value:
|
||||
x = x.encode('utf-8')
|
||||
astuples.append((key, x))
|
||||
else:
|
||||
if isinstance(value, six.text_type) and callable(value.encode):
|
||||
value = value.encode('utf-8')
|
||||
astuples.append((key, value))
|
||||
return '?' + urlencode(astuples)
|
||||
|
||||
def _log_response(self, resp, content):
|
||||
"""Logs debugging information about the response if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info('--response-start--')
|
||||
for h, v in six.iteritems(resp):
|
||||
LOGGER.info('%s: %s', h, v)
|
||||
if content:
|
||||
LOGGER.info(content)
|
||||
LOGGER.info('--response-end--')
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
self._log_response(resp, content)
|
||||
# Error handling is TBD, for example, do we retry
|
||||
# for some operation/error combinations?
|
||||
if resp.status < 300:
|
||||
if resp.status == 204:
|
||||
# A 204: No Content response should be treated differently
|
||||
# to all the other success states
|
||||
return self.no_content_response
|
||||
return self.deserialize(content)
|
||||
else:
|
||||
LOGGER.debug('Content from bad request was: %s' % content)
|
||||
raise HttpError(resp, content)
|
||||
|
||||
def serialize(self, body_value):
|
||||
"""Perform the actual Python object serialization.
|
||||
|
||||
Args:
|
||||
body_value: object, the request body as a Python object.
|
||||
|
||||
Returns:
|
||||
string, the body in serialized form.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def deserialize(self, content):
|
||||
"""Perform the actual deserialization from response string to Python
|
||||
object.
|
||||
|
||||
Args:
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class JsonModel(BaseModel):
|
||||
"""Model class for JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request and response bodies.
|
||||
"""
|
||||
accept = 'application/json'
|
||||
content_type = 'application/json'
|
||||
alt_param = 'json'
|
||||
|
||||
def __init__(self, data_wrapper=False):
|
||||
"""Construct a JsonModel.
|
||||
|
||||
Args:
|
||||
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
||||
"""
|
||||
self._data_wrapper = data_wrapper
|
||||
|
||||
def serialize(self, body_value):
|
||||
if (isinstance(body_value, dict) and 'data' not in body_value and
|
||||
self._data_wrapper):
|
||||
body_value = {'data': body_value}
|
||||
return json.dumps(body_value)
|
||||
|
||||
def deserialize(self, content):
|
||||
try:
|
||||
content = content.decode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
body = json.loads(content)
|
||||
if self._data_wrapper and isinstance(body, dict) and 'data' in body:
|
||||
body = body['data']
|
||||
return body
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RawModel(JsonModel):
|
||||
"""Model class for requests that don't return JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
accept = '*/*'
|
||||
content_type = 'application/json'
|
||||
alt_param = None
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ''
|
||||
|
||||
|
||||
class MediaModel(JsonModel):
|
||||
"""Model class for requests that return Media.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
accept = '*/*'
|
||||
content_type = 'application/json'
|
||||
alt_param = 'media'
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ''
|
||||
|
||||
|
||||
class ProtocolBufferModel(BaseModel):
|
||||
"""Model class for protocol buffers.
|
||||
|
||||
Serializes and de-serializes the binary protocol buffer sent in the HTTP
|
||||
request and response bodies.
|
||||
"""
|
||||
accept = 'application/x-protobuf'
|
||||
content_type = 'application/x-protobuf'
|
||||
alt_param = 'proto'
|
||||
|
||||
def __init__(self, protocol_buffer):
|
||||
"""Constructs a ProtocolBufferModel.
|
||||
|
||||
The serialzed protocol buffer returned in an HTTP response will be
|
||||
de-serialized using the given protocol buffer class.
|
||||
|
||||
Args:
|
||||
protocol_buffer: The protocol buffer class used to de-serialize a
|
||||
response from the API.
|
||||
"""
|
||||
self._protocol_buffer = protocol_buffer
|
||||
|
||||
def serialize(self, body_value):
|
||||
return body_value.SerializeToString()
|
||||
|
||||
def deserialize(self, content):
|
||||
return self._protocol_buffer.FromString(content)
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return self._protocol_buffer()
|
||||
|
||||
|
||||
def makepatch(original, modified):
|
||||
"""Create a patch object.
|
||||
|
||||
Some methods support PATCH, an efficient way to send updates to a resource.
|
||||
This method allows the easy construction of patch bodies by looking at the
|
||||
differences between a resource before and after it was modified.
|
||||
|
||||
Args:
|
||||
original: object, the original deserialized resource
|
||||
modified: object, the modified deserialized resource
|
||||
Returns:
|
||||
An object that contains only the changes from original to modified, in a
|
||||
form suitable to pass to a PATCH method.
|
||||
|
||||
Example usage:
|
||||
item = service.activities().get(postid=postid, userid=userid).execute()
|
||||
original = copy.deepcopy(item)
|
||||
item['object']['content'] = 'This is updated.'
|
||||
service.activities.patch(postid=postid, userid=userid,
|
||||
body=makepatch(original, item)).execute()
|
||||
"""
|
||||
patch = {}
|
||||
for key, original_value in six.iteritems(original):
|
||||
modified_value = modified.get(key, None)
|
||||
if modified_value is None:
|
||||
# Use None to signal that the element is deleted
|
||||
patch[key] = None
|
||||
elif original_value != modified_value:
|
||||
if type(original_value) == type({}):
|
||||
# Recursively descend objects
|
||||
patch[key] = makepatch(original_value, modified_value)
|
||||
else:
|
||||
# In the case of simple types or arrays we just replace
|
||||
patch[key] = modified_value
|
||||
else:
|
||||
# Don't add anything to patch if there's no change
|
||||
pass
|
||||
for key in modified:
|
||||
if key not in original:
|
||||
patch[key] = modified[key]
|
||||
|
||||
return patch
|
||||
@@ -1,107 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Utilities for making samples.
|
||||
|
||||
Consolidates a lot of code commonly repeated in sample applications.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
__all__ = ['init']
|
||||
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from googleapiclient import discovery
|
||||
from googleapiclient.http import build_http
|
||||
|
||||
try:
|
||||
from oauth2client import client
|
||||
from oauth2client import file
|
||||
from oauth2client import tools
|
||||
except ImportError:
|
||||
raise ImportError('googleapiclient.sample_tools requires oauth2client. Please install oauth2client and try again.')
|
||||
|
||||
|
||||
def init(argv, name, version, doc, filename, scope=None, parents=[], discovery_filename=None):
|
||||
"""A common initialization routine for samples.
|
||||
|
||||
Many of the sample applications do the same initialization, which has now
|
||||
been consolidated into this function. This function uses common idioms found
|
||||
in almost all the samples, i.e. for an API with name 'apiname', the
|
||||
credentials are stored in a file named apiname.dat, and the
|
||||
client_secrets.json file is stored in the same directory as the application
|
||||
main file.
|
||||
|
||||
Args:
|
||||
argv: list of string, the command-line parameters of the application.
|
||||
name: string, name of the API.
|
||||
version: string, version of the API.
|
||||
doc: string, description of the application. Usually set to __doc__.
|
||||
file: string, filename of the application. Usually set to __file__.
|
||||
parents: list of argparse.ArgumentParser, additional command-line flags.
|
||||
scope: string, The OAuth scope used.
|
||||
discovery_filename: string, name of local discovery file (JSON). Use when discovery doc not available via URL.
|
||||
|
||||
Returns:
|
||||
A tuple of (service, flags), where service is the service object and flags
|
||||
is the parsed command-line flags.
|
||||
"""
|
||||
if scope is None:
|
||||
scope = 'https://www.googleapis.com/auth/' + name
|
||||
|
||||
# Parser command-line arguments.
|
||||
parent_parsers = [tools.argparser]
|
||||
parent_parsers.extend(parents)
|
||||
parser = argparse.ArgumentParser(
|
||||
description=doc,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=parent_parsers)
|
||||
flags = parser.parse_args(argv[1:])
|
||||
|
||||
# Name of a file containing the OAuth 2.0 information for this
|
||||
# application, including client_id and client_secret, which are found
|
||||
# on the API Access tab on the Google APIs
|
||||
# Console <http://code.google.com/apis/console>.
|
||||
client_secrets = os.path.join(os.path.dirname(filename),
|
||||
'client_secrets.json')
|
||||
|
||||
# Set up a Flow object to be used if we need to authenticate.
|
||||
flow = client.flow_from_clientsecrets(client_secrets,
|
||||
scope=scope,
|
||||
message=tools.message_if_missing(client_secrets))
|
||||
|
||||
# Prepare credentials, and authorize HTTP object with them.
|
||||
# If the credentials don't exist or are invalid run through the native client
|
||||
# flow. The Storage object will ensure that if successful the good
|
||||
# credentials will get written back to a file.
|
||||
storage = file.Storage(name + '.dat')
|
||||
credentials = storage.get()
|
||||
if credentials is None or credentials.invalid:
|
||||
credentials = tools.run_flow(flow, storage, flags)
|
||||
http = credentials.authorize(http=build_http())
|
||||
|
||||
if discovery_filename is None:
|
||||
# Construct a service object via the discovery service.
|
||||
service = discovery.build(name, version, http=http)
|
||||
else:
|
||||
# Construct a service object using a local discovery document file.
|
||||
with open(discovery_filename) as discovery_file:
|
||||
service = discovery.build_from_document(
|
||||
discovery_file.read(),
|
||||
base='https://www.googleapis.com/',
|
||||
http=http)
|
||||
return (service, flags)
|
||||
@@ -1,314 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Schema processing for discovery based APIs
|
||||
|
||||
Schemas holds an APIs discovery schemas. It can return those schema as
|
||||
deserialized JSON objects, or pretty print them as prototype objects that
|
||||
conform to the schema.
|
||||
|
||||
For example, given the schema:
|
||||
|
||||
schema = \"\"\"{
|
||||
"Foo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string",
|
||||
"description": "ETag of the collection."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Type of the collection ('calendar#acl').",
|
||||
"default": "calendar#acl"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"description": "Token used to access the next
|
||||
page of this result. Omitted if no further results are available."
|
||||
}
|
||||
}
|
||||
}
|
||||
}\"\"\"
|
||||
|
||||
s = Schemas(schema)
|
||||
print s.prettyPrintByName('Foo')
|
||||
|
||||
Produces the following output:
|
||||
|
||||
{
|
||||
"nextPageToken": "A String", # Token used to access the
|
||||
# next page of this result. Omitted if no further results are available.
|
||||
"kind": "A String", # Type of the collection ('calendar#acl').
|
||||
"etag": "A String", # ETag of the collection.
|
||||
},
|
||||
|
||||
The constructor takes a discovery document in which to look up named schema.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
import six
|
||||
|
||||
# TODO(jcgregorio) support format, enum, minimum, maximum
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import copy
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
|
||||
|
||||
class Schemas(object):
|
||||
"""Schemas for an API."""
|
||||
|
||||
def __init__(self, discovery):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
discovery: object, Deserialized discovery document from which we pull
|
||||
out the named schema.
|
||||
"""
|
||||
self.schemas = discovery.get('schemas', {})
|
||||
|
||||
# Cache of pretty printed schemas.
|
||||
self.pretty = {}
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintByName(self, name, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
if name in seen:
|
||||
# Do not fall into an infinite loop over recursive definitions.
|
||||
return '# Object with schema name: %s' % name
|
||||
seen.append(name)
|
||||
|
||||
if name not in self.pretty:
|
||||
self.pretty[name] = _SchemaToStruct(self.schemas[name],
|
||||
seen, dent=dent).to_str(self._prettyPrintByName)
|
||||
|
||||
seen.pop()
|
||||
|
||||
return self.pretty[name]
|
||||
|
||||
def prettyPrintByName(self, name):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintSchema(self, schema, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
|
||||
|
||||
def prettyPrintSchema(self, schema):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintSchema(schema, dent=1)[:-2]
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""Get deserialized JSON schema from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Schema name.
|
||||
default: object, return value if name not found.
|
||||
"""
|
||||
return self.schemas.get(name, default)
|
||||
|
||||
|
||||
class _SchemaToStruct(object):
|
||||
"""Convert schema to a prototype object."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, schema, seen, dent=0):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list, List of names of schema already seen while parsing. Used to
|
||||
handle recursive definitions.
|
||||
dent: int, Initial indentation depth.
|
||||
"""
|
||||
# The result of this parsing kept as list of strings.
|
||||
self.value = []
|
||||
|
||||
# The final value of the parsing.
|
||||
self.string = None
|
||||
|
||||
# The parsed JSON schema.
|
||||
self.schema = schema
|
||||
|
||||
# Indentation level.
|
||||
self.dent = dent
|
||||
|
||||
# Method that when called returns a prototype object for the schema with
|
||||
# the given name.
|
||||
self.from_cache = None
|
||||
|
||||
# List of names of schema already seen while parsing.
|
||||
self.seen = seen
|
||||
|
||||
def emit(self, text):
|
||||
"""Add text as a line to the output.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text, '\n'])
|
||||
|
||||
def emitBegin(self, text):
|
||||
"""Add text to the output, but with no line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text])
|
||||
|
||||
def emitEnd(self, text, comment):
|
||||
"""Add text and comment to the output with line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
comment: string, Python comment.
|
||||
"""
|
||||
if comment:
|
||||
divider = '\n' + ' ' * (self.dent + 2) + '# '
|
||||
lines = comment.splitlines()
|
||||
lines = [x.rstrip() for x in lines]
|
||||
comment = divider.join(lines)
|
||||
self.value.extend([text, ' # ', comment, '\n'])
|
||||
else:
|
||||
self.value.extend([text, '\n'])
|
||||
|
||||
def indent(self):
|
||||
"""Increase indentation level."""
|
||||
self.dent += 1
|
||||
|
||||
def undent(self):
|
||||
"""Decrease indentation level."""
|
||||
self.dent -= 1
|
||||
|
||||
def _to_str_impl(self, schema):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema file.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
"""
|
||||
stype = schema.get('type')
|
||||
if stype == 'object':
|
||||
self.emitEnd('{', schema.get('description', ''))
|
||||
self.indent()
|
||||
if 'properties' in schema:
|
||||
for pname, pschema in six.iteritems(schema.get('properties', {})):
|
||||
self.emitBegin('"%s": ' % pname)
|
||||
self._to_str_impl(pschema)
|
||||
elif 'additionalProperties' in schema:
|
||||
self.emitBegin('"a_key": ')
|
||||
self._to_str_impl(schema['additionalProperties'])
|
||||
self.undent()
|
||||
self.emit('},')
|
||||
elif '$ref' in schema:
|
||||
schemaName = schema['$ref']
|
||||
description = schema.get('description', '')
|
||||
s = self.from_cache(schemaName, seen=self.seen)
|
||||
parts = s.splitlines()
|
||||
self.emitEnd(parts[0], description)
|
||||
for line in parts[1:]:
|
||||
self.emit(line.rstrip())
|
||||
elif stype == 'boolean':
|
||||
value = schema.get('default', 'True or False')
|
||||
self.emitEnd('%s,' % str(value), schema.get('description', ''))
|
||||
elif stype == 'string':
|
||||
value = schema.get('default', 'A String')
|
||||
self.emitEnd('"%s",' % str(value), schema.get('description', ''))
|
||||
elif stype == 'integer':
|
||||
value = schema.get('default', '42')
|
||||
self.emitEnd('%s,' % str(value), schema.get('description', ''))
|
||||
elif stype == 'number':
|
||||
value = schema.get('default', '3.14')
|
||||
self.emitEnd('%s,' % str(value), schema.get('description', ''))
|
||||
elif stype == 'null':
|
||||
self.emitEnd('None,', schema.get('description', ''))
|
||||
elif stype == 'any':
|
||||
self.emitEnd('"",', schema.get('description', ''))
|
||||
elif stype == 'array':
|
||||
self.emitEnd('[', schema.get('description'))
|
||||
self.indent()
|
||||
self.emitBegin('')
|
||||
self._to_str_impl(schema['items'])
|
||||
self.undent()
|
||||
self.emit('],')
|
||||
else:
|
||||
self.emit('Unknown type! %s' % stype)
|
||||
self.emitEnd('', '')
|
||||
|
||||
self.string = ''.join(self.value)
|
||||
return self.string
|
||||
|
||||
def to_str(self, from_cache):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
from_cache: callable(name, seen), Callable that retrieves an object
|
||||
prototype for a schema with the given name. Seen is a list of schema
|
||||
names already seen as we recursively descend the schema definition.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
The lines of the code will all be properly indented.
|
||||
"""
|
||||
self.from_cache = from_cache
|
||||
return self._to_str_impl(self.schema)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,110 +0,0 @@
|
||||
"""
|
||||
iri2uri
|
||||
|
||||
Converts an IRI to a URI.
|
||||
|
||||
"""
|
||||
__author__ = "Joe Gregorio (joe@bitworking.org)"
|
||||
__copyright__ = "Copyright 2006, Joe Gregorio"
|
||||
__contributors__ = []
|
||||
__version__ = "1.0.0"
|
||||
__license__ = "MIT"
|
||||
__history__ = """
|
||||
"""
|
||||
|
||||
import urlparse
|
||||
|
||||
|
||||
# Convert an IRI to a URI following the rules in RFC 3987
|
||||
#
|
||||
# The characters we need to enocde and escape are defined in the spec:
|
||||
#
|
||||
# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
|
||||
# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
|
||||
# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
|
||||
# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
|
||||
# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
|
||||
# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
|
||||
# / %xD0000-DFFFD / %xE1000-EFFFD
|
||||
|
||||
escape_range = [
|
||||
(0xA0, 0xD7FF),
|
||||
(0xE000, 0xF8FF),
|
||||
(0xF900, 0xFDCF),
|
||||
(0xFDF0, 0xFFEF),
|
||||
(0x10000, 0x1FFFD),
|
||||
(0x20000, 0x2FFFD),
|
||||
(0x30000, 0x3FFFD),
|
||||
(0x40000, 0x4FFFD),
|
||||
(0x50000, 0x5FFFD),
|
||||
(0x60000, 0x6FFFD),
|
||||
(0x70000, 0x7FFFD),
|
||||
(0x80000, 0x8FFFD),
|
||||
(0x90000, 0x9FFFD),
|
||||
(0xA0000, 0xAFFFD),
|
||||
(0xB0000, 0xBFFFD),
|
||||
(0xC0000, 0xCFFFD),
|
||||
(0xD0000, 0xDFFFD),
|
||||
(0xE1000, 0xEFFFD),
|
||||
(0xF0000, 0xFFFFD),
|
||||
(0x100000, 0x10FFFD),
|
||||
]
|
||||
|
||||
def encode(c):
|
||||
retval = c
|
||||
i = ord(c)
|
||||
for low, high in escape_range:
|
||||
if i < low:
|
||||
break
|
||||
if i >= low and i <= high:
|
||||
retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')])
|
||||
break
|
||||
return retval
|
||||
|
||||
|
||||
def iri2uri(uri):
|
||||
"""Convert an IRI to a URI. Note that IRIs must be
|
||||
passed in a unicode strings. That is, do not utf-8 encode
|
||||
the IRI before passing it into the function."""
|
||||
if isinstance(uri ,unicode):
|
||||
(scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
|
||||
authority = authority.encode('idna')
|
||||
# For each character in 'ucschar' or 'iprivate'
|
||||
# 1. encode as utf-8
|
||||
# 2. then %-encode each octet of that utf-8
|
||||
uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
|
||||
uri = "".join([encode(c) for c in uri])
|
||||
return uri
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def test_uris(self):
|
||||
"""Test that URIs are invariant under the transformation."""
|
||||
invariant = [
|
||||
u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
|
||||
u"http://www.ietf.org/rfc/rfc2396.txt",
|
||||
u"ldap://[2001:db8::7]/c=GB?objectClass?one",
|
||||
u"mailto:John.Doe@example.com",
|
||||
u"news:comp.infosystems.www.servers.unix",
|
||||
u"tel:+1-816-555-1212",
|
||||
u"telnet://192.0.2.16:80/",
|
||||
u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
|
||||
for uri in invariant:
|
||||
self.assertEqual(uri, iri2uri(uri))
|
||||
|
||||
def test_iri(self):
|
||||
""" Test that the right type of escaping is done for each part of the URI."""
|
||||
self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
|
||||
self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}"))
|
||||
self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}"))
|
||||
self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}"))
|
||||
self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))
|
||||
self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")))
|
||||
self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8')))
|
||||
|
||||
unittest.main()
|
||||
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
"""SocksiPy - Python SOCKS module.
|
||||
Version 1.00
|
||||
|
||||
Copyright 2006 Dan-Haim. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of Dan Haim nor the names of his contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
|
||||
|
||||
|
||||
This module provides a standard socket-like interface for Python
|
||||
for tunneling connections through SOCKS proxies.
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
Minor modifications made by Christopher Gilbert (http://motomastyle.com/)
|
||||
for use in PyLoris (http://pyloris.sourceforge.net/)
|
||||
|
||||
Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
|
||||
mainly to merge bug fixes found in Sourceforge
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
if getattr(socket, 'socket', None) is None:
|
||||
raise ImportError('socket.socket missing, proxy support unusable')
|
||||
|
||||
PROXY_TYPE_SOCKS4 = 1
|
||||
PROXY_TYPE_SOCKS5 = 2
|
||||
PROXY_TYPE_HTTP = 3
|
||||
PROXY_TYPE_HTTP_NO_TUNNEL = 4
|
||||
|
||||
_defaultproxy = None
|
||||
_orgsocket = socket.socket
|
||||
|
||||
class ProxyError(Exception): pass
|
||||
class GeneralProxyError(ProxyError): pass
|
||||
class Socks5AuthError(ProxyError): pass
|
||||
class Socks5Error(ProxyError): pass
|
||||
class Socks4Error(ProxyError): pass
|
||||
class HTTPError(ProxyError): pass
|
||||
|
||||
_generalerrors = ("success",
|
||||
"invalid data",
|
||||
"not connected",
|
||||
"not available",
|
||||
"bad proxy type",
|
||||
"bad input")
|
||||
|
||||
_socks5errors = ("succeeded",
|
||||
"general SOCKS server failure",
|
||||
"connection not allowed by ruleset",
|
||||
"Network unreachable",
|
||||
"Host unreachable",
|
||||
"Connection refused",
|
||||
"TTL expired",
|
||||
"Command not supported",
|
||||
"Address type not supported",
|
||||
"Unknown error")
|
||||
|
||||
_socks5autherrors = ("succeeded",
|
||||
"authentication is required",
|
||||
"all offered authentication methods were rejected",
|
||||
"unknown username or invalid password",
|
||||
"unknown error")
|
||||
|
||||
_socks4errors = ("request granted",
|
||||
"request rejected or failed",
|
||||
"request rejected because SOCKS server cannot connect to identd on the client",
|
||||
"request rejected because the client program and identd report different user-ids",
|
||||
"unknown error")
|
||||
|
||||
def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
|
||||
"""setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
|
||||
Sets a default proxy which all further socksocket objects will use,
|
||||
unless explicitly changed.
|
||||
"""
|
||||
global _defaultproxy
|
||||
_defaultproxy = (proxytype, addr, port, rdns, username, password)
|
||||
|
||||
def wrapmodule(module):
|
||||
"""wrapmodule(module)
|
||||
Attempts to replace a module's socket library with a SOCKS socket. Must set
|
||||
a default proxy using setdefaultproxy(...) first.
|
||||
This will only work on modules that import socket directly into the namespace;
|
||||
most of the Python Standard Library falls into this category.
|
||||
"""
|
||||
if _defaultproxy != None:
|
||||
module.socket.socket = socksocket
|
||||
else:
|
||||
raise GeneralProxyError((4, "no proxy specified"))
|
||||
|
||||
class socksocket(socket.socket):
|
||||
"""socksocket([family[, type[, proto]]]) -> socket object
|
||||
Open a SOCKS enabled socket. The parameters are the same as
|
||||
those of the standard socket init. In order for SOCKS to work,
|
||||
you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
|
||||
"""
|
||||
|
||||
def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None):
|
||||
_orgsocket.__init__(self, family, type, proto, _sock)
|
||||
if _defaultproxy != None:
|
||||
self.__proxy = _defaultproxy
|
||||
else:
|
||||
self.__proxy = (None, None, None, None, None, None)
|
||||
self.__proxysockname = None
|
||||
self.__proxypeername = None
|
||||
self.__httptunnel = True
|
||||
|
||||
def __recvall(self, count):
|
||||
"""__recvall(count) -> data
|
||||
Receive EXACTLY the number of bytes requested from the socket.
|
||||
Blocks until the required number of bytes have been received.
|
||||
"""
|
||||
data = self.recv(count)
|
||||
while len(data) < count:
|
||||
d = self.recv(count-len(data))
|
||||
if not d: raise GeneralProxyError((0, "connection closed unexpectedly"))
|
||||
data = data + d
|
||||
return data
|
||||
|
||||
def sendall(self, content, *args):
|
||||
""" override socket.socket.sendall method to rewrite the header
|
||||
for non-tunneling proxies if needed
|
||||
"""
|
||||
if not self.__httptunnel:
|
||||
content = self.__rewriteproxy(content)
|
||||
return super(socksocket, self).sendall(content, *args)
|
||||
|
||||
def __rewriteproxy(self, header):
|
||||
""" rewrite HTTP request headers to support non-tunneling proxies
|
||||
(i.e. those which do not support the CONNECT method).
|
||||
This only works for HTTP (not HTTPS) since HTTPS requires tunneling.
|
||||
"""
|
||||
host, endpt = None, None
|
||||
hdrs = header.split("\r\n")
|
||||
for hdr in hdrs:
|
||||
if hdr.lower().startswith("host:"):
|
||||
host = hdr
|
||||
elif hdr.lower().startswith("get") or hdr.lower().startswith("post"):
|
||||
endpt = hdr
|
||||
if host and endpt:
|
||||
hdrs.remove(host)
|
||||
hdrs.remove(endpt)
|
||||
host = host.split(" ")[1]
|
||||
endpt = endpt.split(" ")
|
||||
if (self.__proxy[4] != None and self.__proxy[5] != None):
|
||||
hdrs.insert(0, self.__getauthheader())
|
||||
hdrs.insert(0, "Host: %s" % host)
|
||||
hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2]))
|
||||
return "\r\n".join(hdrs)
|
||||
|
||||
def __getauthheader(self):
|
||||
auth = self.__proxy[4] + ":" + self.__proxy[5]
|
||||
return "Proxy-Authorization: Basic " + base64.b64encode(auth)
|
||||
|
||||
def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None, headers=None):
|
||||
"""setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
|
||||
Sets the proxy to be used.
|
||||
proxytype - The type of the proxy to be used. Three types
|
||||
are supported: PROXY_TYPE_SOCKS4 (including socks4a),
|
||||
PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
|
||||
addr - The address of the server (IP or DNS).
|
||||
port - The port of the server. Defaults to 1080 for SOCKS
|
||||
servers and 8080 for HTTP proxy servers.
|
||||
rdns - Should DNS queries be preformed on the remote side
|
||||
(rather than the local side). The default is True.
|
||||
Note: This has no effect with SOCKS4 servers.
|
||||
username - Username to authenticate with to the server.
|
||||
The default is no authentication.
|
||||
password - Password to authenticate with to the server.
|
||||
Only relevant when username is also provided.
|
||||
headers - Additional or modified headers for the proxy connect request.
|
||||
"""
|
||||
self.__proxy = (proxytype, addr, port, rdns, username, password, headers)
|
||||
|
||||
def __negotiatesocks5(self, destaddr, destport):
|
||||
"""__negotiatesocks5(self,destaddr,destport)
|
||||
Negotiates a connection through a SOCKS5 server.
|
||||
"""
|
||||
# First we'll send the authentication packages we support.
|
||||
if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
|
||||
# The username/password details were supplied to the
|
||||
# setproxy method so we support the USERNAME/PASSWORD
|
||||
# authentication (in addition to the standard none).
|
||||
self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
|
||||
else:
|
||||
# No username/password were entered, therefore we
|
||||
# only support connections with no authentication.
|
||||
self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00))
|
||||
# We'll receive the server's response to determine which
|
||||
# method was selected
|
||||
chosenauth = self.__recvall(2)
|
||||
if chosenauth[0:1] != chr(0x05).encode():
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
# Check the chosen authentication method
|
||||
if chosenauth[1:2] == chr(0x00).encode():
|
||||
# No authentication is required
|
||||
pass
|
||||
elif chosenauth[1:2] == chr(0x02).encode():
|
||||
# Okay, we need to perform a basic username/password
|
||||
# authentication.
|
||||
self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5])
|
||||
authstat = self.__recvall(2)
|
||||
if authstat[0:1] != chr(0x01).encode():
|
||||
# Bad response
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
if authstat[1:2] != chr(0x00).encode():
|
||||
# Authentication failed
|
||||
self.close()
|
||||
raise Socks5AuthError((3, _socks5autherrors[3]))
|
||||
# Authentication succeeded
|
||||
else:
|
||||
# Reaching here is always bad
|
||||
self.close()
|
||||
if chosenauth[1] == chr(0xFF).encode():
|
||||
raise Socks5AuthError((2, _socks5autherrors[2]))
|
||||
else:
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
# Now we can request the actual connection
|
||||
req = struct.pack('BBB', 0x05, 0x01, 0x00)
|
||||
# If the given destination address is an IP address, we'll
|
||||
# use the IPv4 address request even if remote resolving was specified.
|
||||
try:
|
||||
ipaddr = socket.inet_aton(destaddr)
|
||||
req = req + chr(0x01).encode() + ipaddr
|
||||
except socket.error:
|
||||
# Well it's not an IP number, so it's probably a DNS name.
|
||||
if self.__proxy[3]:
|
||||
# Resolve remotely
|
||||
ipaddr = None
|
||||
req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr.encode()
|
||||
else:
|
||||
# Resolve locally
|
||||
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
|
||||
req = req + chr(0x01).encode() + ipaddr
|
||||
req = req + struct.pack(">H", destport)
|
||||
self.sendall(req)
|
||||
# Get the response
|
||||
resp = self.__recvall(4)
|
||||
if resp[0:1] != chr(0x05).encode():
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
elif resp[1:2] != chr(0x00).encode():
|
||||
# Connection failed
|
||||
self.close()
|
||||
if ord(resp[1:2])<=8:
|
||||
raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])]))
|
||||
else:
|
||||
raise Socks5Error((9, _socks5errors[9]))
|
||||
# Get the bound address/port
|
||||
elif resp[3:4] == chr(0x01).encode():
|
||||
boundaddr = self.__recvall(4)
|
||||
elif resp[3:4] == chr(0x03).encode():
|
||||
resp = resp + self.recv(1)
|
||||
boundaddr = self.__recvall(ord(resp[4:5]))
|
||||
else:
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
boundport = struct.unpack(">H", self.__recvall(2))[0]
|
||||
self.__proxysockname = (boundaddr, boundport)
|
||||
if ipaddr != None:
|
||||
self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
|
||||
else:
|
||||
self.__proxypeername = (destaddr, destport)
|
||||
|
||||
def getproxysockname(self):
|
||||
"""getsockname() -> address info
|
||||
Returns the bound IP address and port number at the proxy.
|
||||
"""
|
||||
return self.__proxysockname
|
||||
|
||||
def getproxypeername(self):
|
||||
"""getproxypeername() -> address info
|
||||
Returns the IP and port number of the proxy.
|
||||
"""
|
||||
return _orgsocket.getpeername(self)
|
||||
|
||||
def getpeername(self):
|
||||
"""getpeername() -> address info
|
||||
Returns the IP address and port number of the destination
|
||||
machine (note: getproxypeername returns the proxy)
|
||||
"""
|
||||
return self.__proxypeername
|
||||
|
||||
def __negotiatesocks4(self,destaddr,destport):
|
||||
"""__negotiatesocks4(self,destaddr,destport)
|
||||
Negotiates a connection through a SOCKS4 server.
|
||||
"""
|
||||
# Check if the destination address provided is an IP address
|
||||
rmtrslv = False
|
||||
try:
|
||||
ipaddr = socket.inet_aton(destaddr)
|
||||
except socket.error:
|
||||
# It's a DNS name. Check where it should be resolved.
|
||||
if self.__proxy[3]:
|
||||
ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)
|
||||
rmtrslv = True
|
||||
else:
|
||||
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
|
||||
# Construct the request packet
|
||||
req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr
|
||||
# The username parameter is considered userid for SOCKS4
|
||||
if self.__proxy[4] != None:
|
||||
req = req + self.__proxy[4]
|
||||
req = req + chr(0x00).encode()
|
||||
# DNS name if remote resolving is required
|
||||
# NOTE: This is actually an extension to the SOCKS4 protocol
|
||||
# called SOCKS4A and may not be supported in all cases.
|
||||
if rmtrslv:
|
||||
req = req + destaddr + chr(0x00).encode()
|
||||
self.sendall(req)
|
||||
# Get the response from the server
|
||||
resp = self.__recvall(8)
|
||||
if resp[0:1] != chr(0x00).encode():
|
||||
# Bad data
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
if resp[1:2] != chr(0x5A).encode():
|
||||
# Server returned an error
|
||||
self.close()
|
||||
if ord(resp[1:2]) in (91, 92, 93):
|
||||
self.close()
|
||||
raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90]))
|
||||
else:
|
||||
raise Socks4Error((94, _socks4errors[4]))
|
||||
# Get the bound address/port
|
||||
self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0])
|
||||
if rmtrslv != None:
|
||||
self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
|
||||
else:
|
||||
self.__proxypeername = (destaddr, destport)
|
||||
|
||||
def __negotiatehttp(self, destaddr, destport):
|
||||
"""__negotiatehttp(self,destaddr,destport)
|
||||
Negotiates a connection through an HTTP server.
|
||||
"""
|
||||
# If we need to resolve locally, we do this now
|
||||
if not self.__proxy[3]:
|
||||
addr = socket.gethostbyname(destaddr)
|
||||
else:
|
||||
addr = destaddr
|
||||
headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"]
|
||||
wrote_host_header = False
|
||||
wrote_auth_header = False
|
||||
if self.__proxy[6] != None:
|
||||
for key, val in self.__proxy[6].iteritems():
|
||||
headers += [key, ": ", val, "\r\n"]
|
||||
wrote_host_header = (key.lower() == "host")
|
||||
wrote_auth_header = (key.lower() == "proxy-authorization")
|
||||
if not wrote_host_header:
|
||||
headers += ["Host: ", destaddr, "\r\n"]
|
||||
if not wrote_auth_header:
|
||||
if (self.__proxy[4] != None and self.__proxy[5] != None):
|
||||
headers += [self.__getauthheader(), "\r\n"]
|
||||
headers.append("\r\n")
|
||||
self.sendall("".join(headers).encode())
|
||||
# We read the response until we get the string "\r\n\r\n"
|
||||
resp = self.recv(1)
|
||||
while resp.find("\r\n\r\n".encode()) == -1:
|
||||
resp = resp + self.recv(1)
|
||||
# We just need the first line to check if the connection
|
||||
# was successful
|
||||
statusline = resp.splitlines()[0].split(" ".encode(), 2)
|
||||
if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()):
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
try:
|
||||
statuscode = int(statusline[1])
|
||||
except ValueError:
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
if statuscode != 200:
|
||||
self.close()
|
||||
raise HTTPError((statuscode, statusline[2]))
|
||||
self.__proxysockname = ("0.0.0.0", 0)
|
||||
self.__proxypeername = (addr, destport)
|
||||
|
||||
def connect(self, destpair):
|
||||
"""connect(self, despair)
|
||||
Connects to the specified destination through a proxy.
|
||||
destpar - A tuple of the IP/DNS address and the port number.
|
||||
(identical to socket's connect).
|
||||
To select the proxy server use setproxy().
|
||||
"""
|
||||
# Do a minimal input check first
|
||||
if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int):
|
||||
raise GeneralProxyError((5, _generalerrors[5]))
|
||||
if self.__proxy[0] == PROXY_TYPE_SOCKS5:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 1080
|
||||
_orgsocket.connect(self, (self.__proxy[1], portnum))
|
||||
self.__negotiatesocks5(destpair[0], destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 1080
|
||||
_orgsocket.connect(self,(self.__proxy[1], portnum))
|
||||
self.__negotiatesocks4(destpair[0], destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_HTTP:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 8080
|
||||
_orgsocket.connect(self,(self.__proxy[1], portnum))
|
||||
self.__negotiatehttp(destpair[0], destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 8080
|
||||
_orgsocket.connect(self,(self.__proxy[1],portnum))
|
||||
if destpair[1] == 443:
|
||||
self.__negotiatehttp(destpair[0],destpair[1])
|
||||
else:
|
||||
self.__httptunnel = False
|
||||
elif self.__proxy[0] == None:
|
||||
_orgsocket.connect(self, (destpair[0], destpair[1]))
|
||||
else:
|
||||
raise GeneralProxyError((4, _generalerrors[4]))
|
||||
@@ -1 +0,0 @@
|
||||
from realsocket import gaierror, error, getaddrinfo, SOCK_STREAM
|
||||
@@ -1,89 +0,0 @@
|
||||
from __future__ import print_function
|
||||
import unittest
|
||||
import errno
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import nose
|
||||
|
||||
import httplib2
|
||||
from httplib2 import socks
|
||||
from httplib2.test import miniserver
|
||||
|
||||
tinyproxy_cfg = """
|
||||
User "%(user)s"
|
||||
Port %(port)s
|
||||
Listen 127.0.0.1
|
||||
PidFile "%(pidfile)s"
|
||||
LogFile "%(logfile)s"
|
||||
MaxClients 2
|
||||
StartServers 1
|
||||
LogLevel Info
|
||||
"""
|
||||
|
||||
|
||||
class FunctionalProxyHttpTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not socks:
|
||||
raise nose.SkipTest('socks module unavailable')
|
||||
if not subprocess:
|
||||
raise nose.SkipTest('subprocess module unavailable')
|
||||
|
||||
# start a short-lived miniserver so we can get a likely port
|
||||
# for the proxy
|
||||
self.httpd, self.proxyport = miniserver.start_server(
|
||||
miniserver.ThisDirHandler)
|
||||
self.httpd.shutdown()
|
||||
self.httpd, self.port = miniserver.start_server(
|
||||
miniserver.ThisDirHandler)
|
||||
|
||||
self.pidfile = tempfile.mktemp()
|
||||
self.logfile = tempfile.mktemp()
|
||||
fd, self.conffile = tempfile.mkstemp()
|
||||
f = os.fdopen(fd, 'w')
|
||||
our_cfg = tinyproxy_cfg % {'user': os.getlogin(),
|
||||
'pidfile': self.pidfile,
|
||||
'port': self.proxyport,
|
||||
'logfile': self.logfile}
|
||||
f.write(our_cfg)
|
||||
f.close()
|
||||
try:
|
||||
# TODO use subprocess.check_call when 2.4 is dropped
|
||||
ret = subprocess.call(['tinyproxy', '-c', self.conffile])
|
||||
self.assertEqual(0, ret)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise nose.SkipTest('tinyproxy not available')
|
||||
raise
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
try:
|
||||
pid = int(open(self.pidfile).read())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ESRCH:
|
||||
print('\n\n\nTinyProxy Failed to start, log follows:')
|
||||
print(open(self.logfile).read())
|
||||
print('end tinyproxy log\n\n\n')
|
||||
raise
|
||||
map(os.unlink, (self.pidfile,
|
||||
self.logfile,
|
||||
self.conffile))
|
||||
|
||||
def testSimpleProxy(self):
|
||||
proxy_info = httplib2.ProxyInfo(socks.PROXY_TYPE_HTTP,
|
||||
'localhost', self.proxyport)
|
||||
client = httplib2.Http(proxy_info=proxy_info)
|
||||
src = 'miniserver.py'
|
||||
response, body = client.request('http://localhost:%d/%s' %
|
||||
(self.port, src))
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, open(os.path.join(miniserver.HERE, src)).read())
|
||||
lf = open(self.logfile).read()
|
||||
expect = ('Established connection to host "127.0.0.1" '
|
||||
'using file descriptor')
|
||||
self.assertTrue(expect in lf,
|
||||
'tinyproxy did not proxy a request for miniserver')
|
||||
@@ -1,113 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import SimpleHTTPServer
|
||||
import socket
|
||||
import SocketServer
|
||||
import threading
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThisDirHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def translate_path(self, path):
|
||||
path = path.split('?', 1)[0].split('#', 1)[0]
|
||||
return os.path.join(HERE, *filter(None, path.split('/')))
|
||||
|
||||
def log_message(self, s, *args):
|
||||
# output via logging so nose can catch it
|
||||
logger.info(s, *args)
|
||||
|
||||
|
||||
class ShutdownServer(SocketServer.TCPServer):
|
||||
"""Mixin that allows serve_forever to be shut down.
|
||||
|
||||
The methods in this mixin are backported from SocketServer.py in the Python
|
||||
2.6.4 standard library. The mixin is unnecessary in 2.6 and later, when
|
||||
BaseServer supports the shutdown method directly.
|
||||
"""
|
||||
|
||||
def __init__(self, use_tls, *args, **kwargs):
|
||||
self.__use_tls = use_tls
|
||||
SocketServer.TCPServer.__init__(self, *args, **kwargs)
|
||||
self.__is_shut_down = threading.Event()
|
||||
self.__serving = False
|
||||
|
||||
def server_bind(self):
|
||||
SocketServer.TCPServer.server_bind(self)
|
||||
if self.__use_tls:
|
||||
import ssl
|
||||
self.socket = ssl.wrap_socket(self.socket,
|
||||
os.path.join(os.path.dirname(__file__), 'server.key'),
|
||||
os.path.join(os.path.dirname(__file__), 'server.pem'),
|
||||
True
|
||||
)
|
||||
|
||||
|
||||
def serve_forever(self, poll_interval=0.1):
|
||||
"""Handle one request at a time until shutdown.
|
||||
|
||||
Polls for shutdown every poll_interval seconds. Ignores
|
||||
self.timeout. If you need to do periodic tasks, do them in
|
||||
another thread.
|
||||
"""
|
||||
self.__serving = True
|
||||
self.__is_shut_down.clear()
|
||||
while self.__serving:
|
||||
r, w, e = select.select([self.socket], [], [], poll_interval)
|
||||
if r:
|
||||
self._handle_request_noblock()
|
||||
self.__is_shut_down.set()
|
||||
|
||||
def shutdown(self):
|
||||
"""Stops the serve_forever loop.
|
||||
|
||||
Blocks until the loop has finished. This must be called while
|
||||
serve_forever() is running in another thread, or it will deadlock.
|
||||
"""
|
||||
self.__serving = False
|
||||
self.__is_shut_down.wait()
|
||||
|
||||
def handle_request(self):
|
||||
"""Handle one request, possibly blocking.
|
||||
|
||||
Respects self.timeout.
|
||||
"""
|
||||
# Support people who used socket.settimeout() to escape
|
||||
# handle_request before self.timeout was available.
|
||||
timeout = self.socket.gettimeout()
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
elif self.timeout is not None:
|
||||
timeout = min(timeout, self.timeout)
|
||||
fd_sets = select.select([self], [], [], timeout)
|
||||
if not fd_sets[0]:
|
||||
self.handle_timeout()
|
||||
return
|
||||
self._handle_request_noblock()
|
||||
|
||||
def _handle_request_noblock(self):
|
||||
"""Handle one request, without blocking.
|
||||
|
||||
I assume that select.select has returned that the socket is
|
||||
readable before this function was called, so there should be
|
||||
no risk of blocking in get_request().
|
||||
"""
|
||||
try:
|
||||
request, client_address = self.get_request()
|
||||
except socket.error:
|
||||
return
|
||||
if self.verify_request(request, client_address):
|
||||
try:
|
||||
self.process_request(request, client_address)
|
||||
except:
|
||||
self.handle_error(request, client_address)
|
||||
self.close_request(request)
|
||||
|
||||
|
||||
def start_server(handler, use_tls=False):
|
||||
httpd = ShutdownServer(use_tls, ("", 0), handler)
|
||||
threading.Thread(target=httpd.serve_forever).start()
|
||||
_, port = httpd.socket.getsockname()
|
||||
return httpd, port
|
||||
@@ -1,19 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBzCCAe+gAwIBAgIJAIw94zvO7fk1MA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
|
||||
BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNjA2MDQwMjMxMTRaFw0yNjA2MDIwMjMx
|
||||
MTRaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
|
||||
BQADggEPADCCAQoCggEBAK3YNcDIwK/wlTa0/iBARvDFOncQ6Jkk+Ymql1HXny7v
|
||||
mWPFWeLXEW+Zw1NrQEx/SIUGvxpRA+QyhTOhu2Gcwvtqilix/dHgaKgqWEcRYu8m
|
||||
L70uVDPVgB/kfNI8bpXM1Mz8Crjo0tHw5oUSD3wny8SyT6CYlXVmF923L8c2zdN9
|
||||
n9blFgYwxBq2+q+mqOiDErMFbwHES8FNBSWGBXdE1xjBdITtlfeHezmJhj/ylPW1
|
||||
7v8HInsv/WqU9DcJYlFxSnK0SZCLFBM/31Ez8O1gCfMlDUFvJoo59GyFqukUjuO1
|
||||
uB85wpu27gtcLm/J9X1Md71IxbDupV7a0dDoTvbhO4kCAwEAAaNQME4wHQYDVR0O
|
||||
BBYEFIHgAmwppZSKLz2peyFSO2kwVobNMB8GA1UdIwQYMBaAFIHgAmwppZSKLz2p
|
||||
eyFSO2kwVobNMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJxz+AU/
|
||||
Iq8fMEStJ0BgPP1N86W9Jpb7aPMFCYTEZ+nd8hFPhPs4//55J0yIve+1I43MNFFz
|
||||
yflwwCzrIIhZdkvbsyea6CmlTo4jBc4+ihaDGobYnoNzFhavC47n5kYqJ8Ikyb2W
|
||||
OMrmNRiaTeSBl0wQmftnnQCbonenjmE1LDuJtE6bCwfFjfLbMxwdWtp/ymOlXsb5
|
||||
80XcWwcqc12UHWexYwHFzEJmDfncak/8tjHBsLWMJg5p2sVTY9kVt7TYgSIl+mFb
|
||||
4WVGrqZd2uTlJkRQQ4pCl+D+PKwadHuV6YI7oxkeajjcHCgbK/ANwW28MXYho6t6
|
||||
aWVIN4bWHrZ38kE=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,146 +0,0 @@
|
||||
Private TLS server key file used for HTTPS-related unit tests
|
||||
-------------------------------------------------------------
|
||||
|
||||
Public Key Info:
|
||||
Public Key Algorithm: RSA
|
||||
Key Security Level: Medium (2048 bits)
|
||||
|
||||
modulus:
|
||||
00:cc:fb:f1:c5:de:29:29:40:3f:c4:9f:af:da:f6:be
|
||||
27:f4:6a:00:ae:5a:f2:99:c3:5f:7a:e6:9b:cf:d9:08
|
||||
34:01:9b:ea:fb:da:b5:d0:b5:b2:4e:60:b4:0d:8d:05
|
||||
57:e4:2e:04:d4:57:1a:58:3c:0b:3a:ed:67:a3:13:31
|
||||
58:0a:c2:eb:fd:d6:27:ee:07:95:30:35:b5:98:91:c7
|
||||
a5:9b:be:a9:7e:ae:fd:73:c3:6b:21:bc:52:f8:ef:71
|
||||
db:d3:b1:cd:51:df:b3:37:b3:fd:7d:ae:7e:02:38:be
|
||||
8e:6f:45:55:e5:6d:8a:02:cb:36:c4:17:7a:ea:24:9a
|
||||
72:8d:1e:75:03:3a:6f:c4:cb:a0:3a:50:56:32:bb:4c
|
||||
e2:ea:74:f0:96:31:74:b2:c1:03:e8:c3:d4:a3:59:fc
|
||||
7a:cc:68:35:c4:97:eb:aa:46:fa:64:c3:f9:55:59:22
|
||||
b5:2b:3c:96:84:c6:d2:7d:b4:9f:b9:9c:af:d1:20:30
|
||||
7c:e8:60:4e:ee:0a:60:a0:9d:4e:8a:d8:34:74:bd:f2
|
||||
40:bc:d7:c2:b3:1a:b2:bb:d7:a5:4a:4c:65:94:43:82
|
||||
16:9a:8f:76:2a:05:b0:9e:3d:a7:fb:e2:c7:78:25:f7
|
||||
df:ca:08:ee:ec:4f:cd:1a:3c:03:41:ec:91:c5:50:70
|
||||
4b:
|
||||
|
||||
public exponent:
|
||||
01:00:01:
|
||||
|
||||
private exponent:
|
||||
00:ad:01:83:b8:7d:dd:fd:ab:f5:66:2d:64:ce:08:ec
|
||||
cb:6a:15:41:87:e6:c8:d5:10:39:78:d0:43:f7:73:f4
|
||||
e1:77:ee:31:b0:e9:92:04:9a:25:e8:d2:e3:84:80:5e
|
||||
5f:24:fd:d6:23:a5:74:5d:be:27:b8:4f:80:e5:f9:1f
|
||||
ef:6f:fd:be:12:1a:7a:cf:02:65:5f:30:25:99:a4:88
|
||||
7d:74:ea:c1:c1:63:4e:15:33:7d:2b:16:f8:6c:94:23
|
||||
63:e6:d3:2d:38:89:f6:87:f0:08:e5:d7:ad:10:90:f5
|
||||
fb:df:5c:04:b8:43:f0:74:95:31:1e:e5:b6:5f:02:0f
|
||||
bb:55:cb:e1:b5:48:9f:1f:d3:1b:55:a7:bc:39:2b:8e
|
||||
6d:14:64:3b:bf:e8:ca:6b:af:a9:f3:13:9a:c6:df:15
|
||||
ef:6d:17:4e:8e:67:6c:41:20:dc:6b:08:0d:b9:14:cd
|
||||
83:10:62:15:e6:b0:89:5d:37:fb:f6:fd:f0:bf:3b:9c
|
||||
0b:e9:fd:b8:de:e4:64:90:bf:81:d5:59:2c:30:43:07
|
||||
b9:60:8c:d0:ac:4f:95:87:aa:38:62:bd:c7:06:a7:c4
|
||||
2d:08:c1:3c:86:10:c7:8e:1e:df:58:bf:95:ad:39:84
|
||||
a0:2b:13:e2:18:e6:4a:80:f0:bc:04:50:bd:7d:cf:23
|
||||
a1:
|
||||
|
||||
prime1:
|
||||
00:f0:8f:ad:2f:c9:64:f3:0d:2c:aa:06:17:05:8f:2f
|
||||
d5:cb:92:22:90:05:66:3c:78:75:9d:7b:4c:6a:af:a9
|
||||
1e:d6:28:4f:13:0e:3a:e7:31:49:3d:87:ef:2c:17:70
|
||||
be:69:b3:42:82:6d:9c:b4:13:0a:e4:bc:8c:0f:1a:bd
|
||||
04:b6:a0:be:ba:12:15:bf:04:db:91:1c:26:91:d6:d7
|
||||
f2:ff:2f:0e:5f:96:a1:7c:4b:90:a8:2f:07:2a:cb:dc
|
||||
40:a0:0b:1d:2a:1d:48:98:bd:4a:6b:9d:5c:69:b0:2b
|
||||
6e:9b:2c:b2:a9:cb:28:fe:fa:7f:93:eb:20:c8:59:d0
|
||||
11:
|
||||
|
||||
prime2:
|
||||
00:da:23:c0:3e:82:4c:88:7c:d4:fb:de:24:45:eb:9c
|
||||
ae:2c:80:2d:52:a6:95:05:33:b9:d8:c1:7b:52:01:62
|
||||
11:e6:b6:c6:0d:56:a3:68:39:26:9a:90:08:95:12:a9
|
||||
1c:59:f6:0b:1d:af:6d:c0:c6:9b:2e:7a:62:98:21:36
|
||||
e1:15:4c:e6:6d:a4:08:ac:90:af:57:86:71:78:2e:0e
|
||||
cf:59:0f:35:79:cb:6a:a2:e2:30:2a:a8:f2:84:68:bc
|
||||
8a:f2:48:3b:07:d5:a5:34:f3:d3:ec:25:61:38:f1:0a
|
||||
07:f7:7e:29:61:e4:15:01:80:e3:7b:bd:63:9c:2e:16
|
||||
9b:
|
||||
|
||||
coefficient:
|
||||
00:cb:b4:d2:9f:b4:04:db:8c:54:e6:ae:a9:28:a0:c9
|
||||
70:ad:7a:94:72:5e:86:33:91:d9:43:61:2b:4d:55:e8
|
||||
b7:25:d2:cd:db:1e:c4:56:95:68:85:e2:9b:4f:31:24
|
||||
3a:40:06:41:1c:aa:7a:31:13:fa:07:e0:a6:59:c3:d1
|
||||
d2:c5:2c:6a:82:98:bb:a1:59:c0:6f:ad:d7:2e:ed:5a
|
||||
64:5f:e6:ea:4a:ee:45:29:d9:0f:96:b3:39:f7:ab:57
|
||||
97:aa:c9:f7:b6:9c:c0:51:5d:9f:01:2c:ec:58:8d:06
|
||||
6a:19:d0:33:74:11:6a:25:7c:8f:b7:31:d2:97:05:02
|
||||
6f:
|
||||
|
||||
exp1:
|
||||
00:87:60:43:95:1d:e0:0a:8b:82:74:18:43:42:64:a7
|
||||
05:c8:ae:ef:76:5f:23:7e:aa:47:7e:1d:52:0e:c3:d6
|
||||
07:bd:7b:27:ac:d0:98:43:5c:d0:1b:a9:70:e6:3e:36
|
||||
bb:61:5e:78:f2:4f:5f:1d:53:8e:10:d5:2e:78:9d:92
|
||||
7b:a1:8e:ea:66:6a:21:04:c3:66:10:ce:67:c2:30:c6
|
||||
8c:40:21:2a:14:8e:ff:47:a4:7a:be:ba:e0:6c:ac:16
|
||||
c1:e3:8e:fd:95:a2:af:25:0d:79:61:00:48:6e:4d:ae
|
||||
d3:6a:ce:07:a9:57:e4:35:41:a1:24:0b:f1:01:ee:d1
|
||||
11:
|
||||
|
||||
exp2:
|
||||
00:ca:ca:bd:a7:de:fe:43:4c:b9:bb:c4:d2:37:e6:47
|
||||
ec:6c:16:65:0c:17:2d:26:7e:e5:e1:2a:4d:f8:f8:ac
|
||||
31:34:28:ea:89:ef:e7:4d:b7:03:ba:60:f8:79:8d:b5
|
||||
85:53:e4:b6:84:cc:57:de:05:44:b2:ba:b7:f9:f1:b6
|
||||
d1:1d:3a:36:65:eb:3e:dd:1e:4c:c3:b3:8a:bd:4d:24
|
||||
1b:83:11:ee:86:e1:a2:aa:f6:58:0c:f0:af:34:85:21
|
||||
f2:92:36:b0:1a:22:75:c9:7a:7b:a3:67:44:b0:e8:f4
|
||||
88:5f:7e:fb:fd:b3:4a:0b:f1:c4:89:7e:91:a1:d9:fe
|
||||
cd:
|
||||
|
||||
|
||||
Public Key ID: 92:D5:B4:2A:B6:A8:64:67:2C:2A:08:DB:51:B8:97:86:5E:44:CD:6C
|
||||
Public key's random art:
|
||||
+--[ RSA 2048]----+
|
||||
| + . |
|
||||
| . E o . |
|
||||
| o . . o |
|
||||
| . o o . |
|
||||
| = .= S |
|
||||
|. +.=o + |
|
||||
|o++== . |
|
||||
|++o= |
|
||||
|o . |
|
||||
+-----------------+
|
||||
|
||||
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpgIBAAKCAQEAzPvxxd4pKUA/xJ+v2va+J/RqAK5a8pnDX3rmm8/ZCDQBm+r7
|
||||
2rXQtbJOYLQNjQVX5C4E1FcaWDwLOu1noxMxWArC6/3WJ+4HlTA1tZiRx6Wbvql+
|
||||
rv1zw2shvFL473Hb07HNUd+zN7P9fa5+Aji+jm9FVeVtigLLNsQXeuokmnKNHnUD
|
||||
Om/Ey6A6UFYyu0zi6nTwljF0ssED6MPUo1n8esxoNcSX66pG+mTD+VVZIrUrPJaE
|
||||
xtJ9tJ+5nK/RIDB86GBO7gpgoJ1Oitg0dL3yQLzXwrMasrvXpUpMZZRDghaaj3Yq
|
||||
BbCePaf74sd4Jfffygju7E/NGjwDQeyRxVBwSwIDAQABAoIBAQCtAYO4fd39q/Vm
|
||||
LWTOCOzLahVBh+bI1RA5eNBD93P04XfuMbDpkgSaJejS44SAXl8k/dYjpXRdvie4
|
||||
T4Dl+R/vb/2+Ehp6zwJlXzAlmaSIfXTqwcFjThUzfSsW+GyUI2Pm0y04ifaH8Ajl
|
||||
160QkPX731wEuEPwdJUxHuW2XwIPu1XL4bVInx/TG1WnvDkrjm0UZDu/6Mprr6nz
|
||||
E5rG3xXvbRdOjmdsQSDcawgNuRTNgxBiFeawiV03+/b98L87nAvp/bje5GSQv4HV
|
||||
WSwwQwe5YIzQrE+Vh6o4Yr3HBqfELQjBPIYQx44e31i/la05hKArE+IY5kqA8LwE
|
||||
UL19zyOhAoGBAPCPrS/JZPMNLKoGFwWPL9XLkiKQBWY8eHWde0xqr6ke1ihPEw46
|
||||
5zFJPYfvLBdwvmmzQoJtnLQTCuS8jA8avQS2oL66EhW/BNuRHCaR1tfy/y8OX5ah
|
||||
fEuQqC8HKsvcQKALHSodSJi9SmudXGmwK26bLLKpyyj++n+T6yDIWdARAoGBANoj
|
||||
wD6CTIh81PveJEXrnK4sgC1SppUFM7nYwXtSAWIR5rbGDVajaDkmmpAIlRKpHFn2
|
||||
Cx2vbcDGmy56YpghNuEVTOZtpAiskK9XhnF4Lg7PWQ81ectqouIwKqjyhGi8ivJI
|
||||
OwfVpTTz0+wlYTjxCgf3filh5BUBgON7vWOcLhabAoGBAIdgQ5Ud4AqLgnQYQ0Jk
|
||||
pwXIru92XyN+qkd+HVIOw9YHvXsnrNCYQ1zQG6lw5j42u2FeePJPXx1TjhDVLnid
|
||||
knuhjupmaiEEw2YQzmfCMMaMQCEqFI7/R6R6vrrgbKwWweOO/ZWiryUNeWEASG5N
|
||||
rtNqzgepV+Q1QaEkC/EB7tERAoGBAMrKvafe/kNMubvE0jfmR+xsFmUMFy0mfuXh
|
||||
Kk34+KwxNCjqie/nTbcDumD4eY21hVPktoTMV94FRLK6t/nxttEdOjZl6z7dHkzD
|
||||
s4q9TSQbgxHuhuGiqvZYDPCvNIUh8pI2sBoidcl6e6NnRLDo9Ihffvv9s0oL8cSJ
|
||||
fpGh2f7NAoGBAMu00p+0BNuMVOauqSigyXCtepRyXoYzkdlDYStNVei3JdLN2x7E
|
||||
VpVoheKbTzEkOkAGQRyqejET+gfgplnD0dLFLGqCmLuhWcBvrdcu7VpkX+bqSu5F
|
||||
KdkPlrM596tXl6rJ97acwFFdnwEs7FiNBmoZ0DN0EWolfI+3MdKXBQJv
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,50 +0,0 @@
|
||||
Public, self-signed TLS server key file used for HTTPS-related unit tests
|
||||
-------------------------------------------------------------------------
|
||||
|
||||
Public Key Information:
|
||||
Public Key Algorithm: RSA
|
||||
Algorithm Security Level: Medium (2048 bits)
|
||||
Modulus (bits 2048):
|
||||
00:cc:fb:f1:c5:de:29:29:40:3f:c4:9f:af:da:f6:be
|
||||
27:f4:6a:00:ae:5a:f2:99:c3:5f:7a:e6:9b:cf:d9:08
|
||||
34:01:9b:ea:fb:da:b5:d0:b5:b2:4e:60:b4:0d:8d:05
|
||||
57:e4:2e:04:d4:57:1a:58:3c:0b:3a:ed:67:a3:13:31
|
||||
58:0a:c2:eb:fd:d6:27:ee:07:95:30:35:b5:98:91:c7
|
||||
a5:9b:be:a9:7e:ae:fd:73:c3:6b:21:bc:52:f8:ef:71
|
||||
db:d3:b1:cd:51:df:b3:37:b3:fd:7d:ae:7e:02:38:be
|
||||
8e:6f:45:55:e5:6d:8a:02:cb:36:c4:17:7a:ea:24:9a
|
||||
72:8d:1e:75:03:3a:6f:c4:cb:a0:3a:50:56:32:bb:4c
|
||||
e2:ea:74:f0:96:31:74:b2:c1:03:e8:c3:d4:a3:59:fc
|
||||
7a:cc:68:35:c4:97:eb:aa:46:fa:64:c3:f9:55:59:22
|
||||
b5:2b:3c:96:84:c6:d2:7d:b4:9f:b9:9c:af:d1:20:30
|
||||
7c:e8:60:4e:ee:0a:60:a0:9d:4e:8a:d8:34:74:bd:f2
|
||||
40:bc:d7:c2:b3:1a:b2:bb:d7:a5:4a:4c:65:94:43:82
|
||||
16:9a:8f:76:2a:05:b0:9e:3d:a7:fb:e2:c7:78:25:f7
|
||||
df:ca:08:ee:ec:4f:cd:1a:3c:03:41:ec:91:c5:50:70
|
||||
4b
|
||||
Exponent (bits 24):
|
||||
01:00:01
|
||||
|
||||
Public Key Usage:
|
||||
|
||||
Public Key ID: 92d5b42ab6a864672c2a08db51b897865e44cd6c
|
||||
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC+zCCAeOgAwIBAgIJAISbkoXpX75CMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDAeFw0xNjA2MTcxNDA1NTlaFw0yNjA2MTUxNDA1NTlaMBQx
|
||||
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBAMz78cXeKSlAP8Sfr9r2vif0agCuWvKZw1965pvP2Qg0AZvq+9q10LWyTmC0
|
||||
DY0FV+QuBNRXGlg8CzrtZ6MTMVgKwuv91ifuB5UwNbWYkcelm76pfq79c8NrIbxS
|
||||
+O9x29OxzVHfszez/X2ufgI4vo5vRVXlbYoCyzbEF3rqJJpyjR51AzpvxMugOlBW
|
||||
MrtM4up08JYxdLLBA+jD1KNZ/HrMaDXEl+uqRvpkw/lVWSK1KzyWhMbSfbSfuZyv
|
||||
0SAwfOhgTu4KYKCdTorYNHS98kC818KzGrK716VKTGWUQ4IWmo92KgWwnj2n++LH
|
||||
eCX338oI7uxPzRo8A0HskcVQcEsCAwEAAaNQME4wHQYDVR0OBBYEFFdXD8Z8k0et
|
||||
ZNyM4e4WypNnGlcCMB8GA1UdIwQYMBaAFFdXD8Z8k0etZNyM4e4WypNnGlcCMAwG
|
||||
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAC4FsnK1Ph/JpdoqSRTCJiVM
|
||||
MPFaaavKEEyYdAKPk/Acmb9vf07sqsT+OZg/0obsZG9LxJb7x0iAnhfM3aS+CmO9
|
||||
Ym2lXeFDaJ2bHooB9MsG2C3+n8lJUMwxm7Cqpff/lpCK6Z+6MGPx3GRs6HUEl34k
|
||||
BB5pue2vqhtFQ03UdHMpAK0M7n3TloAWbFb1a/JmqzTbsQ0oaMHGoECQEAbaBl+a
|
||||
/up6vA3iZHq+ZPYS1KIx+xuT/SapLcyUtjfhmq1bROVZP4+6EHMsBMnhJBYKxxHy
|
||||
0qKvqJL9X3NQLMgMKKUKzX+BuG2u5aRRyVIqewT/ORjaUr9Y8lU7WlXPf7Ljm6s=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,23 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import httplib2
|
||||
|
||||
from httplib2.test import miniserver
|
||||
|
||||
|
||||
class HttpSmokeTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.httpd, self.port = miniserver.start_server(
|
||||
miniserver.ThisDirHandler)
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
|
||||
def testGetFile(self):
|
||||
client = httplib2.Http()
|
||||
src = 'miniserver.py'
|
||||
response, body = client.request('http://localhost:%d/%s' %
|
||||
(self.port, src))
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, open(os.path.join(miniserver.HERE, src)).read())
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Tests for httplib2 when the socket module is missing.
|
||||
|
||||
This helps ensure compatibility with environments such as AppEngine.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import httplib2
|
||||
|
||||
class MissingSocketTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._oldsocks = httplib2.socks
|
||||
httplib2.socks = None
|
||||
|
||||
def tearDown(self):
|
||||
httplib2.socks = self._oldsocks
|
||||
|
||||
def testProxyDisabled(self):
|
||||
proxy_info = httplib2.ProxyInfo('blah',
|
||||
'localhost', 0)
|
||||
client = httplib2.Http(proxy_info=proxy_info)
|
||||
self.assertRaises(httplib2.ProxiesUnavailableError,
|
||||
client.request, 'http://localhost:-1/')
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
from __future__ import print_function
|
||||
import BaseHTTPServer
|
||||
import logging
|
||||
import os.path
|
||||
import ssl
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import httplib2
|
||||
from httplib2.test import miniserver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KeepAliveHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""
|
||||
Request handler that keeps the HTTP connection open, so that the test can
|
||||
inspect the resulting SSL connection object
|
||||
"""
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("Connection", "keep-alive")
|
||||
self.end_headers()
|
||||
|
||||
self.close_connection = 0
|
||||
|
||||
def log_message(self, s, *args):
|
||||
# output via logging so nose can catch it
|
||||
logger.info(s, *args)
|
||||
|
||||
|
||||
class HttpsContextTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if sys.version_info < (2, 7, 9):
|
||||
if hasattr(self, "skipTest"):
|
||||
self.skipTest("SSLContext requires Python 2.7.9")
|
||||
else:
|
||||
return
|
||||
|
||||
self.ca_certs_path = os.path.join(os.path.dirname(__file__), 'server.pem')
|
||||
self.httpd, self.port = miniserver.start_server(KeepAliveHandler, True)
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
|
||||
def testHttpsContext(self):
|
||||
client = httplib2.Http(ca_certs=self.ca_certs_path)
|
||||
|
||||
# Establish connection to local server
|
||||
client.request('https://localhost:%d/' % (self.port))
|
||||
|
||||
# Verify that connection uses a TLS context with the correct hostname
|
||||
conn = client.connections['https:localhost:%d' % self.port]
|
||||
|
||||
self.assertIsInstance(conn.sock, ssl.SSLSocket)
|
||||
self.assertTrue(hasattr(conn.sock, 'context'))
|
||||
self.assertIsInstance(conn.sock.context, ssl.SSLContext)
|
||||
self.assertTrue(conn.sock.context.check_hostname)
|
||||
self.assertEqual(conn.sock.server_hostname, 'localhost')
|
||||
self.assertEqual(conn.sock.context.verify_mode, ssl.CERT_REQUIRED)
|
||||
self.assertEqual(conn.sock.context.protocol, ssl.PROTOCOL_SSLv23)
|
||||
|
||||
def test_ssl_hostname_mismatch_repeat(self):
|
||||
# https://github.com/httplib2/httplib2/issues/5
|
||||
|
||||
# FIXME(temoto): as of 2017-01-05 this is only a reference code, not useful test.
|
||||
# Because it doesn't provoke described error on my machine.
|
||||
# Instead `SSLContext.wrap_socket` raises `ssl.CertificateError`
|
||||
# which was also added to original patch.
|
||||
|
||||
# url host is intentionally different, we provoke ssl hostname mismatch error
|
||||
url = 'https://127.0.0.1:%d/' % (self.port,)
|
||||
http = httplib2.Http(ca_certs=self.ca_certs_path, proxy_info=None)
|
||||
|
||||
def once():
|
||||
try:
|
||||
http.request(url)
|
||||
assert False, 'expected certificate hostname mismatch error'
|
||||
except Exception as e:
|
||||
print('%s errno=%s' % (repr(e), getattr(e, 'errno', None)))
|
||||
|
||||
once()
|
||||
once()
|
||||
@@ -1,24 +1,28 @@
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('httplib2/cacerts.txt', 'httplib2/cacerts.txt', 'DATA')]
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
a.datas += [('email-settings-v2.json', 'email-settings-v2.json', 'DATA')]
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
|
||||
# dynamically determine where httplib2/cacerts.txt lives
|
||||
import importlib
|
||||
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
|
||||
a.datas += [('httplib2/cacerts.txt', os.path.join(proot, 'cacerts.txt'), 'DATA')]
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('httplib2/cacerts.txt', 'httplib2/cacerts.txt', 'DATA')]
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
a.datas += [('email-settings-v2.json', 'email-settings-v2.json', 'DATA')]
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
|
||||
# dynamically determine where httplib2/cacerts.txt lives
|
||||
import importlib
|
||||
proot = os.path.dirname(importlib.import_module('httplib2').__file__)
|
||||
a.datas += [('httplib2/cacerts.txt', os.path.join(proot, 'cacerts.txt'), 'DATA')]
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Client library for using OAuth2, especially with Google APIs."""
|
||||
|
||||
__version__ = '4.1.2'
|
||||
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
|
||||
@@ -1,341 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import base64
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POSITIONAL_WARNING = 'WARNING'
|
||||
POSITIONAL_EXCEPTION = 'EXCEPTION'
|
||||
POSITIONAL_IGNORE = 'IGNORE'
|
||||
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
|
||||
POSITIONAL_IGNORE])
|
||||
|
||||
positional_parameters_enforcement = POSITIONAL_WARNING
|
||||
|
||||
_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
|
||||
_IS_DIR_MESSAGE = '{0}: Is a directory'
|
||||
_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
|
||||
|
||||
|
||||
def positional(max_positional_args):
|
||||
"""A decorator to declare that only the first N arguments my be positional.
|
||||
|
||||
This decorator makes it easy to support Python 3 style keyword-only
|
||||
parameters. For example, in Python 3 it is possible to write::
|
||||
|
||||
def fn(pos1, *, kwonly1=None, kwonly1=None):
|
||||
...
|
||||
|
||||
All named parameters after ``*`` must be a keyword::
|
||||
|
||||
fn(10, 'kw1', 'kw2') # Raises exception.
|
||||
fn(10, kwonly1='kw1') # Ok.
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
|
||||
To define a function like above, do::
|
||||
|
||||
@positional(1)
|
||||
def fn(pos1, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
If no default value is provided to a keyword argument, it becomes a
|
||||
required keyword argument::
|
||||
|
||||
@positional(0)
|
||||
def fn(required_kw):
|
||||
...
|
||||
|
||||
This must be called with the keyword parameter::
|
||||
|
||||
fn() # Raises exception.
|
||||
fn(10) # Raises exception.
|
||||
fn(required_kw=10) # Ok.
|
||||
|
||||
When defining instance or class methods always remember to account for
|
||||
``self`` and ``cls``::
|
||||
|
||||
class MyClass(object):
|
||||
|
||||
@positional(2)
|
||||
def my_method(self, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@positional(2)
|
||||
def my_method(cls, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
The positional decorator behavior is controlled by
|
||||
``_helpers.positional_parameters_enforcement``, which may be set to
|
||||
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
||||
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
||||
nothing, respectively, if a declaration is violated.
|
||||
|
||||
Args:
|
||||
max_positional_arguments: Maximum number of positional arguments. All
|
||||
parameters after the this index must be
|
||||
keyword only.
|
||||
|
||||
Returns:
|
||||
A decorator that prevents using arguments after max_positional_args
|
||||
from being used as positional parameters.
|
||||
|
||||
Raises:
|
||||
TypeError: if a key-word only argument is provided as a positional
|
||||
parameter, but only if
|
||||
_helpers.positional_parameters_enforcement is set to
|
||||
POSITIONAL_EXCEPTION.
|
||||
"""
|
||||
|
||||
def positional_decorator(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def positional_wrapper(*args, **kwargs):
|
||||
if len(args) > max_positional_args:
|
||||
plural_s = ''
|
||||
if max_positional_args != 1:
|
||||
plural_s = 's'
|
||||
message = ('{function}() takes at most {args_max} positional '
|
||||
'argument{plural} ({args_given} given)'.format(
|
||||
function=wrapped.__name__,
|
||||
args_max=max_positional_args,
|
||||
args_given=len(args),
|
||||
plural=plural_s))
|
||||
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
||||
raise TypeError(message)
|
||||
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
||||
logger.warning(message)
|
||||
return wrapped(*args, **kwargs)
|
||||
return positional_wrapper
|
||||
|
||||
if isinstance(max_positional_args, six.integer_types):
|
||||
return positional_decorator
|
||||
else:
|
||||
args, _, _, defaults = inspect.getargspec(max_positional_args)
|
||||
return positional(len(args) - len(defaults))(max_positional_args)
|
||||
|
||||
|
||||
def scopes_to_string(scopes):
|
||||
"""Converts scope value to a string.
|
||||
|
||||
If scopes is a string then it is simply passed through. If scopes is an
|
||||
iterable then a string is returned that is all the individual scopes
|
||||
concatenated with spaces.
|
||||
|
||||
Args:
|
||||
scopes: string or iterable of strings, the scopes.
|
||||
|
||||
Returns:
|
||||
The scopes formatted as a single string.
|
||||
"""
|
||||
if isinstance(scopes, six.string_types):
|
||||
return scopes
|
||||
else:
|
||||
return ' '.join(scopes)
|
||||
|
||||
|
||||
def string_to_scopes(scopes):
|
||||
"""Converts stringifed scope value to a list.
|
||||
|
||||
If scopes is a list then it is simply passed through. If scopes is an
|
||||
string then a list of each individual scope is returned.
|
||||
|
||||
Args:
|
||||
scopes: a string or iterable of strings, the scopes.
|
||||
|
||||
Returns:
|
||||
The scopes in a list.
|
||||
"""
|
||||
if not scopes:
|
||||
return []
|
||||
elif isinstance(scopes, six.string_types):
|
||||
return scopes.split(' ')
|
||||
else:
|
||||
return scopes
|
||||
|
||||
|
||||
def parse_unique_urlencoded(content):
|
||||
"""Parses unique key-value parameters from urlencoded content.
|
||||
|
||||
Args:
|
||||
content: string, URL-encoded key-value pairs.
|
||||
|
||||
Returns:
|
||||
dict, The key-value pairs from ``content``.
|
||||
|
||||
Raises:
|
||||
ValueError: if one of the keys is repeated.
|
||||
"""
|
||||
urlencoded_params = urllib.parse.parse_qs(content)
|
||||
params = {}
|
||||
for key, value in six.iteritems(urlencoded_params):
|
||||
if len(value) != 1:
|
||||
msg = ('URL-encoded content contains a repeated value:'
|
||||
'%s -> %s' % (key, ', '.join(value)))
|
||||
raise ValueError(msg)
|
||||
params[key] = value[0]
|
||||
return params
|
||||
|
||||
|
||||
def update_query_params(uri, params):
|
||||
"""Updates a URI with new query parameters.
|
||||
|
||||
If a given key from ``params`` is repeated in the ``uri``, then
|
||||
the URI will be considered invalid and an error will occur.
|
||||
|
||||
If the URI is valid, then each value from ``params`` will
|
||||
replace the corresponding value in the query parameters (if
|
||||
it exists).
|
||||
|
||||
Args:
|
||||
uri: string, A valid URI, with potential existing query parameters.
|
||||
params: dict, A dictionary of query parameters.
|
||||
|
||||
Returns:
|
||||
The same URI but with the new query parameters added.
|
||||
"""
|
||||
parts = urllib.parse.urlparse(uri)
|
||||
query_params = parse_unique_urlencoded(parts.query)
|
||||
query_params.update(params)
|
||||
new_query = urllib.parse.urlencode(query_params)
|
||||
new_parts = parts._replace(query=new_query)
|
||||
return urllib.parse.urlunparse(new_parts)
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
"""Adds a query parameter to a url.
|
||||
|
||||
Replaces the current value if it already exists in the URL.
|
||||
|
||||
Args:
|
||||
url: string, url to add the query parameter to.
|
||||
name: string, query parameter name.
|
||||
value: string, query parameter value.
|
||||
|
||||
Returns:
|
||||
Updated query parameter. Does not update the url if value is None.
|
||||
"""
|
||||
if value is None:
|
||||
return url
|
||||
else:
|
||||
return update_query_params(url, {name: value})
|
||||
|
||||
|
||||
def validate_file(filename):
|
||||
if os.path.islink(filename):
|
||||
raise IOError(_SYM_LINK_MESSAGE.format(filename))
|
||||
elif os.path.isdir(filename):
|
||||
raise IOError(_IS_DIR_MESSAGE.format(filename))
|
||||
elif not os.path.isfile(filename):
|
||||
warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
|
||||
|
||||
|
||||
def _parse_pem_key(raw_key_input):
|
||||
"""Identify and extract PEM keys.
|
||||
|
||||
Determines whether the given key is in the format of PEM key, and extracts
|
||||
the relevant part of the key if it is.
|
||||
|
||||
Args:
|
||||
raw_key_input: The contents of a private key file (either PEM or
|
||||
PKCS12).
|
||||
|
||||
Returns:
|
||||
string, The actual key if the contents are from a PEM file, or
|
||||
else None.
|
||||
"""
|
||||
offset = raw_key_input.find(b'-----BEGIN ')
|
||||
if offset != -1:
|
||||
return raw_key_input[offset:]
|
||||
|
||||
|
||||
def _json_encode(data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
|
||||
def _to_bytes(value, encoding='ascii'):
|
||||
"""Converts a string value to bytes, if necessary.
|
||||
|
||||
Unfortunately, ``six.b`` is insufficient for this task since in
|
||||
Python2 it does not modify ``unicode`` objects.
|
||||
|
||||
Args:
|
||||
value: The string/bytes value to be converted.
|
||||
encoding: The encoding to use to convert unicode to bytes. Defaults
|
||||
to "ascii", which will not allow any characters from ordinals
|
||||
larger than 127. Other useful values are "latin-1", which
|
||||
which will only allows byte ordinals (up to 255) and "utf-8",
|
||||
which will encode any unicode that needs to be.
|
||||
|
||||
Returns:
|
||||
The original value converted to bytes (if unicode) or as passed in
|
||||
if it started out as bytes.
|
||||
|
||||
Raises:
|
||||
ValueError if the value could not be converted to bytes.
|
||||
"""
|
||||
result = (value.encode(encoding)
|
||||
if isinstance(value, six.text_type) else value)
|
||||
if isinstance(result, six.binary_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('{0!r} could not be converted to bytes'.format(value))
|
||||
|
||||
|
||||
def _from_bytes(value):
|
||||
"""Converts bytes to a string value, if necessary.
|
||||
|
||||
Args:
|
||||
value: The string/bytes value to be converted.
|
||||
|
||||
Returns:
|
||||
The original value converted to unicode (if bytes) or as passed in
|
||||
if it started out as unicode.
|
||||
|
||||
Raises:
|
||||
ValueError if the value could not be converted to unicode.
|
||||
"""
|
||||
result = (value.decode('utf-8')
|
||||
if isinstance(value, six.binary_type) else value)
|
||||
if isinstance(result, six.text_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError(
|
||||
'{0!r} could not be converted to unicode'.format(value))
|
||||
|
||||
|
||||
def _urlsafe_b64encode(raw_bytes):
|
||||
raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
|
||||
return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
|
||||
|
||||
|
||||
def _urlsafe_b64decode(b64string):
|
||||
# Guard against unicode strings, which base64 can't handle.
|
||||
b64string = _to_bytes(b64string)
|
||||
padded = b64string + b'=' * (4 - len(b64string) % 4)
|
||||
return base64.urlsafe_b64decode(padded)
|
||||
@@ -1,136 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
"""OpenSSL Crypto-related routines for oauth2client."""
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
class OpenSSLVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey: OpenSSL.crypto.PKey, The public key to verify with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If string,
|
||||
will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
signature = _helpers._to_bytes(signature, encoding='utf-8')
|
||||
try:
|
||||
crypto.verify(self._pubkey, signature, message, 'sha256')
|
||||
return True
|
||||
except crypto.Error:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error: if the key_pem can't be parsed.
|
||||
"""
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
|
||||
else:
|
||||
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
|
||||
return OpenSSLVerifier(pubkey)
|
||||
|
||||
|
||||
class OpenSSLSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return crypto.sign(self._key, message, 'sha256')
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password=b'notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PKCS12 or PEM format.
|
||||
password: string, password for the private key file.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error if the key can't be parsed.
|
||||
"""
|
||||
key = _helpers._to_bytes(key)
|
||||
parsed_pem_key = _helpers._parse_pem_key(key)
|
||||
if parsed_pem_key:
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
|
||||
else:
|
||||
password = _helpers._to_bytes(password, encoding='utf-8')
|
||||
pkey = crypto.load_pkcs12(key, password).get_privatekey()
|
||||
return OpenSSLSigner(pkey)
|
||||
|
||||
|
||||
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
|
||||
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
|
||||
|
||||
Args:
|
||||
private_key_bytes: Bytes. PKCS#12 key in DER format.
|
||||
private_key_password: String. Password for PKCS#12 key.
|
||||
|
||||
Returns:
|
||||
String. PEM contents of ``private_key_bytes``.
|
||||
"""
|
||||
private_key_password = _helpers._to_bytes(private_key_password)
|
||||
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
pkcs12.get_privatekey())
|
||||
@@ -1,67 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
|
||||
Public Clients
|
||||
|
||||
See RFC7636.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
|
||||
def code_verifier(n_bytes=64):
|
||||
"""
|
||||
Generates a 'code_verifier' as described in section 4.1 of RFC 7636.
|
||||
|
||||
This is a 'high-entropy cryptographic random string' that will be
|
||||
impractical for an attacker to guess.
|
||||
|
||||
Args:
|
||||
n_bytes: integer between 31 and 96, inclusive. default: 64
|
||||
number of bytes of entropy to include in verifier.
|
||||
|
||||
Returns:
|
||||
Bytestring, representing urlsafe base64-encoded random data.
|
||||
"""
|
||||
verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=')
|
||||
# https://tools.ietf.org/html/rfc7636#section-4.1
|
||||
# minimum length of 43 characters and a maximum length of 128 characters.
|
||||
if len(verifier) < 43:
|
||||
raise ValueError("Verifier too short. n_bytes must be > 30.")
|
||||
elif len(verifier) > 128:
|
||||
raise ValueError("Verifier too long. n_bytes must be < 97.")
|
||||
else:
|
||||
return verifier
|
||||
|
||||
|
||||
def code_challenge(verifier):
|
||||
"""
|
||||
Creates a 'code_challenge' as described in section 4.2 of RFC 7636
|
||||
by taking the sha256 hash of the verifier and then urlsafe
|
||||
base64-encoding it.
|
||||
|
||||
Args:
|
||||
verifier: bytestring, representing a code_verifier as generated by
|
||||
code_verifier().
|
||||
|
||||
Returns:
|
||||
Bytestring, representing a urlsafe base64-encoded sha256 hash digest,
|
||||
without '=' padding.
|
||||
"""
|
||||
digest = hashlib.sha256(verifier).digest()
|
||||
return base64.urlsafe_b64encode(digest).rstrip(b'=')
|
||||
@@ -1,184 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Pure Python crypto-related routines for oauth2client.
|
||||
|
||||
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
|
||||
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
|
||||
certificates.
|
||||
"""
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import pem
|
||||
from pyasn1_modules.rfc2459 import Certificate
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
import six
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
_PKCS12_ERROR = r"""\
|
||||
PKCS12 format is not supported by the RSA library.
|
||||
Either install PyOpenSSL, or please convert .p12 format
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
|
||||
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
|
||||
'-----END RSA PRIVATE KEY-----')
|
||||
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
|
||||
'-----END PRIVATE KEY-----')
|
||||
_PKCS8_SPEC = PrivateKeyInfo()
|
||||
|
||||
|
||||
def _bit_list_to_bytes(bit_list):
|
||||
"""Converts an iterable of 1's and 0's to bytes.
|
||||
|
||||
Combines the list 8 at a time, treating each group of 8 bits
|
||||
as a single byte.
|
||||
"""
|
||||
num_bits = len(bit_list)
|
||||
byte_vals = bytearray()
|
||||
for start in six.moves.xrange(0, num_bits, 8):
|
||||
curr_bits = bit_list[start:start + 8]
|
||||
char_val = sum(val * digit
|
||||
for val, digit in zip(_POW2, curr_bits))
|
||||
byte_vals.append(char_val)
|
||||
return bytes(byte_vals)
|
||||
|
||||
|
||||
class RsaVerifier(object):
|
||||
"""Verifies the signature on a message.
|
||||
|
||||
Args:
|
||||
pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
|
||||
"""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If
|
||||
string, will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
try:
|
||||
return rsa.pkcs1.verify(message, signature, self._pubkey)
|
||||
except (ValueError, rsa.pkcs1.VerificationError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key_pem, is_x509_cert):
|
||||
"""Construct an RsaVerifier instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
RsaVerifier instance.
|
||||
|
||||
Raises:
|
||||
ValueError: if the key_pem can't be parsed. In either case, error
|
||||
will begin with 'No PEM start marker'. If
|
||||
``is_x509_cert`` is True, will fail to find the
|
||||
"-----BEGIN CERTIFICATE-----" error, otherwise fails
|
||||
to find "-----BEGIN RSA PUBLIC KEY-----".
|
||||
"""
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
|
||||
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
|
||||
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
|
||||
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
|
||||
else:
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
|
||||
return cls(pubkey)
|
||||
|
||||
|
||||
class RsaSigner(object):
|
||||
"""Signs messages with a private key.
|
||||
|
||||
Args:
|
||||
pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
|
||||
def __init__(self, pkey):
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key, password='notasecret'):
|
||||
"""Construct an RsaSigner instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
RsaSigner instance.
|
||||
|
||||
Raises:
|
||||
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
|
||||
PEM format.
|
||||
"""
|
||||
key = _helpers._from_bytes(key) # pem expects str in Py3
|
||||
marker_id, key_bytes = pem.readPemBlocksFromFile(
|
||||
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
|
||||
|
||||
if marker_id == 0:
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
|
||||
format='DER')
|
||||
elif marker_id == 1:
|
||||
key_info, remaining = decoder.decode(
|
||||
key_bytes, asn1Spec=_PKCS8_SPEC)
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
pkey_info = key_info.getComponentByName('privateKey')
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
|
||||
format='DER')
|
||||
else:
|
||||
raise ValueError('No key could be detected.')
|
||||
|
||||
return cls(pkey)
|
||||
@@ -1,124 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
"""pyCrypto Crypto-related routines for oauth2client."""
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Util.asn1 import DerSequence
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
class PyCryptoVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
|
||||
with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return PKCS1_v1_5.new(self._pubkey).verify(
|
||||
SHA256.new(message), signature)
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
"""
|
||||
if is_x509_cert:
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
pemLines = key_pem.replace(b' ', b'').split()
|
||||
certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
|
||||
certSeq = DerSequence()
|
||||
certSeq.decode(certDer)
|
||||
tbsSeq = DerSequence()
|
||||
tbsSeq.decode(certSeq[0])
|
||||
pubkey = RSA.importKey(tbsSeq[6])
|
||||
else:
|
||||
pubkey = RSA.importKey(key_pem)
|
||||
return PyCryptoVerifier(pubkey)
|
||||
|
||||
|
||||
class PyCryptoSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: string, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password='notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if the key isn't in PEM format.
|
||||
"""
|
||||
parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
|
||||
if parsed_pem_key:
|
||||
pkey = RSA.importKey(parsed_pem_key)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'No key in PEM format was detected. This implementation '
|
||||
'can only use the PyCrypto library for keys in PEM '
|
||||
'format.')
|
||||
return PyCryptoSigner(pkey)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,173 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Utilities for reading OAuth 2.0 client secret files.
|
||||
|
||||
A client_secrets.json file contains all the information needed to interact with
|
||||
an OAuth 2.0 protected service.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
|
||||
# Properties that make a client_secrets.json file valid.
|
||||
TYPE_WEB = 'web'
|
||||
TYPE_INSTALLED = 'installed'
|
||||
|
||||
VALID_CLIENT = {
|
||||
TYPE_WEB: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
TYPE_INSTALLED: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
|
||||
class InvalidClientSecretsError(Error):
|
||||
"""Format of ClientSecrets file is invalid."""
|
||||
|
||||
|
||||
def _validate_clientsecrets(clientsecrets_dict):
|
||||
"""Validate parsed client secrets from a file.
|
||||
|
||||
Args:
|
||||
clientsecrets_dict: dict, a dictionary holding the client secrets.
|
||||
|
||||
Returns:
|
||||
tuple, a string of the client type and the information parsed
|
||||
from the file.
|
||||
"""
|
||||
_INVALID_FILE_FORMAT_MSG = (
|
||||
'Invalid file format. See '
|
||||
'https://developers.google.com/api-client-library/'
|
||||
'python/guide/aaa_client_secrets')
|
||||
|
||||
if clientsecrets_dict is None:
|
||||
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
|
||||
try:
|
||||
(client_type, client_info), = clientsecrets_dict.items()
|
||||
except (ValueError, AttributeError):
|
||||
raise InvalidClientSecretsError(
|
||||
_INVALID_FILE_FORMAT_MSG + ' '
|
||||
'Expected a JSON object with a single property for a "web" or '
|
||||
'"installed" application')
|
||||
|
||||
if client_type not in VALID_CLIENT:
|
||||
raise InvalidClientSecretsError(
|
||||
'Unknown client type: {0}.'.format(client_type))
|
||||
|
||||
for prop_name in VALID_CLIENT[client_type]['required']:
|
||||
if prop_name not in client_info:
|
||||
raise InvalidClientSecretsError(
|
||||
'Missing property "{0}" in a client type of "{1}".'.format(
|
||||
prop_name, client_type))
|
||||
for prop_name in VALID_CLIENT[client_type]['string']:
|
||||
if client_info[prop_name].startswith('[['):
|
||||
raise InvalidClientSecretsError(
|
||||
'Property "{0}" is not configured.'.format(prop_name))
|
||||
return client_type, client_info
|
||||
|
||||
|
||||
def load(fp):
|
||||
obj = json.load(fp)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loads(s):
|
||||
obj = json.loads(s)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def _loadfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fp:
|
||||
obj = json.load(fp)
|
||||
except IOError as exc:
|
||||
raise InvalidClientSecretsError('Error opening file', exc.filename,
|
||||
exc.strerror, exc.errno)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loadfile(filename, cache=None):
|
||||
"""Loading of client_secrets JSON file, optionally backed by a cache.
|
||||
|
||||
Typical cache storage would be App Engine memcache service,
|
||||
but you can pass in any other cache client that implements
|
||||
these methods:
|
||||
|
||||
* ``get(key, namespace=ns)``
|
||||
* ``set(key, value, namespace=ns)``
|
||||
|
||||
Usage::
|
||||
|
||||
# without caching
|
||||
client_type, client_info = loadfile('secrets.json')
|
||||
# using App Engine memcache service
|
||||
from google.appengine.api import memcache
|
||||
client_type, client_info = loadfile('secrets.json', cache=memcache)
|
||||
|
||||
Args:
|
||||
filename: string, Path to a client_secrets.json file on a filesystem.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. If not specified, the file is always being loaded from
|
||||
a filesystem.
|
||||
|
||||
Raises:
|
||||
InvalidClientSecretsError: In case of a validation error or some
|
||||
I/O failure. Can happen only on cache miss.
|
||||
|
||||
Returns:
|
||||
(client_type, client_info) tuple, as _loadfile() normally would.
|
||||
JSON contents is validated only during first load. Cache hits are not
|
||||
validated.
|
||||
"""
|
||||
_SECRET_NAMESPACE = 'oauth2client:secrets#ns'
|
||||
|
||||
if not cache:
|
||||
return _loadfile(filename)
|
||||
|
||||
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
|
||||
if obj is None:
|
||||
client_type, client_info = _loadfile(filename)
|
||||
obj = {client_type: client_info}
|
||||
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
|
||||
|
||||
return next(six.iteritems(obj))
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Contributed modules.
|
||||
|
||||
Contrib contains modules that are not considered part of the core oauth2client
|
||||
library but provide additional functionality. These modules are intended to
|
||||
make it easier to use oauth2client.
|
||||
"""
|
||||
@@ -1,163 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Google App Engine utilities helper.
|
||||
|
||||
Classes that directly require App Engine's ndb library. Provided
|
||||
as a separate module in case of failure to import ndb while
|
||||
other App Engine libraries are present.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from google.appengine.ext import ndb
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
NDB_KEY = ndb.Key
|
||||
"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
NDB_MODEL = ndb.Model
|
||||
"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SiteXsrfSecretKeyNDB(ndb.Model):
|
||||
"""NDB Model for storage for the sites XSRF secret key.
|
||||
|
||||
Since this model uses the same kind as SiteXsrfSecretKey, it can be
|
||||
used interchangeably. This simply provides an NDB model for interacting
|
||||
with the same data the DB model interacts with.
|
||||
|
||||
There should only be one instance stored of this model, the one used
|
||||
for the site.
|
||||
"""
|
||||
secret = ndb.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'SiteXsrfSecretKey'
|
||||
|
||||
|
||||
class FlowNDBProperty(ndb.PickleProperty):
|
||||
"""App Engine NDB datastore Property for Flow.
|
||||
|
||||
Serves the same purpose as the DB FlowProperty, but for NDB models.
|
||||
Since PickleProperty inherits from BlobProperty, the underlying
|
||||
representation of the data in the datastore will be the same as in the
|
||||
DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper Flow object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Flow.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise TypeError(
|
||||
'Property {0} must be convertible to a flow '
|
||||
'instance; received: {1}.'.format(self._name, value))
|
||||
|
||||
|
||||
class CredentialsNDBProperty(ndb.BlobProperty):
|
||||
"""App Engine NDB datastore Property for Credentials.
|
||||
|
||||
Serves the same purpose as the DB CredentialsProperty, but for NDB
|
||||
models. Since CredentialsProperty stores data as a blob and this
|
||||
inherits from BlobProperty, the data in the datastore will be the same
|
||||
as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of Credentials
|
||||
and subclasses.
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper credentials object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Credentials.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise TypeError(
|
||||
'Property {0} must be convertible to a credentials '
|
||||
'instance; received: {1}.'.format(self._name, value))
|
||||
|
||||
def _to_base_type(self, value):
|
||||
"""Converts our validated value to a JSON serialized string.
|
||||
|
||||
Args:
|
||||
value: A value to be set in the datastore.
|
||||
|
||||
Returns:
|
||||
A JSON serialized version of the credential, else '' if value
|
||||
is None.
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value.to_json()
|
||||
|
||||
def _from_base_type(self, value):
|
||||
"""Converts our stored JSON string back to the desired type.
|
||||
|
||||
Args:
|
||||
value: A value from the datastore to be converted to the
|
||||
desired type.
|
||||
|
||||
Returns:
|
||||
A deserialized Credentials (or subclass) object, else None if
|
||||
the value can't be parsed.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
# Uses the from_json method of the implied class of value
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
|
||||
class CredentialsNDBModel(ndb.Model):
|
||||
"""NDB Model for storage of OAuth 2.0 Credentials
|
||||
|
||||
Since this model uses the same kind as CredentialsModel and has a
|
||||
property which can serialize and deserialize Credentials correctly, it
|
||||
can be used interchangeably with a CredentialsModel to access, insert
|
||||
and delete the same entities. This simply provides an NDB model for
|
||||
interacting with the same data the DB model interacts with.
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsNDBProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'CredentialsModel'
|
||||
@@ -1,118 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Provides helper methods for talking to the Compute Engine metadata server.
|
||||
|
||||
See https://cloud.google.com/compute/docs/metadata
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
from six.moves import http_client
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
from oauth2client import transport
|
||||
|
||||
|
||||
METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format(
|
||||
os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal'))
|
||||
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
|
||||
|
||||
|
||||
def get(http, path, root=METADATA_ROOT, recursive=None):
|
||||
"""Fetch a resource from the metadata server.
|
||||
|
||||
Args:
|
||||
http: an object to be used to make HTTP requests.
|
||||
path: A string indicating the resource to retrieve. For example,
|
||||
'instance/service-accounts/default'
|
||||
root: A string indicating the full path to the metadata server root.
|
||||
recursive: A boolean indicating whether to do a recursive query of
|
||||
metadata. See
|
||||
https://cloud.google.com/compute/docs/metadata#aggcontents
|
||||
|
||||
Returns:
|
||||
A dictionary if the metadata server returns JSON, otherwise a string.
|
||||
|
||||
Raises:
|
||||
http_client.HTTPException if an error corrured while
|
||||
retrieving metadata.
|
||||
"""
|
||||
url = urlparse.urljoin(root, path)
|
||||
url = _helpers._add_query_parameter(url, 'recursive', recursive)
|
||||
|
||||
response, content = transport.request(
|
||||
http, url, headers=METADATA_HEADERS)
|
||||
|
||||
if response.status == http_client.OK:
|
||||
decoded = _helpers._from_bytes(content)
|
||||
if response['content-type'] == 'application/json':
|
||||
return json.loads(decoded)
|
||||
else:
|
||||
return decoded
|
||||
else:
|
||||
raise http_client.HTTPException(
|
||||
'Failed to retrieve {0} from the Google Compute Engine'
|
||||
'metadata service. Response:\n{1}'.format(url, response))
|
||||
|
||||
|
||||
def get_service_account_info(http, service_account='default'):
|
||||
"""Get information about a service account from the metadata server.
|
||||
|
||||
Args:
|
||||
http: an object to be used to make HTTP requests.
|
||||
service_account: An email specifying the service account for which to
|
||||
look up information. Default will be information for the "default"
|
||||
service account of the current compute engine instance.
|
||||
|
||||
Returns:
|
||||
A dictionary with information about the specified service account,
|
||||
for example:
|
||||
|
||||
{
|
||||
'email': '...',
|
||||
'scopes': ['scope', ...],
|
||||
'aliases': ['default', '...']
|
||||
}
|
||||
"""
|
||||
return get(
|
||||
http,
|
||||
'instance/service-accounts/{0}/'.format(service_account),
|
||||
recursive=True)
|
||||
|
||||
|
||||
def get_token(http, service_account='default'):
|
||||
"""Fetch an oauth token for the
|
||||
|
||||
Args:
|
||||
http: an object to be used to make HTTP requests.
|
||||
service_account: An email specifying the service account this token
|
||||
should represent. Default will be a token for the "default" service
|
||||
account of the current compute engine instance.
|
||||
|
||||
Returns:
|
||||
A tuple of (access token, token expiration), where access token is the
|
||||
access token as a string and token expiration is a datetime object
|
||||
that indicates when the access token will expire.
|
||||
"""
|
||||
token_json = get(
|
||||
http,
|
||||
'instance/service-accounts/{0}/token'.format(service_account))
|
||||
token_expiry = client._UTCNOW() + datetime.timedelta(
|
||||
seconds=token_json['expires_in'])
|
||||
return token_json['access_token'], token_expiry
|
||||
@@ -1,910 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Utilities for Google App Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google App Engine.
|
||||
"""
|
||||
|
||||
import cgi
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import threading
|
||||
|
||||
from google.appengine.api import app_identity
|
||||
from google.appengine.api import memcache
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext.webapp.util import login_required
|
||||
import webapp2 as webapp
|
||||
|
||||
import oauth2client
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import transport
|
||||
from oauth2client.contrib import xsrfutil
|
||||
|
||||
# This is a temporary fix for a Google internal issue.
|
||||
try:
|
||||
from oauth2client.contrib import _appengine_ndb
|
||||
except ImportError: # pragma: NO COVER
|
||||
_appengine_ndb = None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
|
||||
|
||||
XSRF_MEMCACHE_ID = 'xsrf_secret_key'
|
||||
|
||||
if _appengine_ndb is None: # pragma: NO COVER
|
||||
CredentialsNDBModel = None
|
||||
CredentialsNDBProperty = None
|
||||
FlowNDBProperty = None
|
||||
_NDB_KEY = None
|
||||
_NDB_MODEL = None
|
||||
SiteXsrfSecretKeyNDB = None
|
||||
else:
|
||||
CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
|
||||
CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
|
||||
FlowNDBProperty = _appengine_ndb.FlowNDBProperty
|
||||
_NDB_KEY = _appengine_ndb.NDB_KEY
|
||||
_NDB_MODEL = _appengine_ndb.NDB_MODEL
|
||||
SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
|
||||
|
||||
|
||||
def _safe_html(s):
|
||||
"""Escape text to make it safe to display.
|
||||
|
||||
Args:
|
||||
s: string, The text to escape.
|
||||
|
||||
Returns:
|
||||
The escaped text as a string.
|
||||
"""
|
||||
return cgi.escape(s, quote=1).replace("'", ''')
|
||||
|
||||
|
||||
class SiteXsrfSecretKey(db.Model):
|
||||
"""Storage for the sites XSRF secret key.
|
||||
|
||||
There will only be one instance stored of this model, the one used for the
|
||||
site.
|
||||
"""
|
||||
secret = db.StringProperty()
|
||||
|
||||
|
||||
def _generate_new_xsrf_secret_key():
|
||||
"""Returns a random XSRF secret key."""
|
||||
return os.urandom(16).encode("hex")
|
||||
|
||||
|
||||
def xsrf_secret_key():
|
||||
"""Return the secret key for use for XSRF protection.
|
||||
|
||||
If the Site entity does not have a secret key, this method will also create
|
||||
one and persist it.
|
||||
|
||||
Returns:
|
||||
The secret key.
|
||||
"""
|
||||
secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
if not secret:
|
||||
# Load the one and only instance of SiteXsrfSecretKey.
|
||||
model = SiteXsrfSecretKey.get_or_insert(key_name='site')
|
||||
if not model.secret:
|
||||
model.secret = _generate_new_xsrf_secret_key()
|
||||
model.put()
|
||||
secret = model.secret
|
||||
memcache.add(XSRF_MEMCACHE_ID, secret,
|
||||
namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
|
||||
return str(secret)
|
||||
|
||||
|
||||
class AppAssertionCredentials(client.AssertionCredentials):
|
||||
"""Credentials object for App Engine Assertion Grants
|
||||
|
||||
This object will allow an App Engine application to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the App Engine application itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@_helpers.positional(2)
|
||||
def __init__(self, scope, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
**kwargs: optional keyword args, including:
|
||||
service_account_id: service account id of the application. If None
|
||||
or unspecified, the default service account for
|
||||
the app is used.
|
||||
"""
|
||||
self.scope = _helpers.scopes_to_string(scope)
|
||||
self._kwargs = kwargs
|
||||
self.service_account_id = kwargs.get('service_account_id', None)
|
||||
self._service_account_email = None
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(json_data)
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http):
|
||||
"""Refreshes the access token.
|
||||
|
||||
Since the underlying App Engine app_identity implementation does its
|
||||
own caching we can skip all the storage hoops and just to a refresh
|
||||
using the API.
|
||||
|
||||
Args:
|
||||
http: unused HTTP object
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
try:
|
||||
scopes = self.scope.split()
|
||||
(token, _) = app_identity.get_access_token(
|
||||
scopes, service_account_id=self.service_account_id)
|
||||
except app_identity.Error as e:
|
||||
raise client.AccessTokenRefreshError(str(e))
|
||||
self.access_token = token
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError('Cannot serialize credentials '
|
||||
'for Google App Engine.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self.scope
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self._kwargs)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return app_identity.sign_blob(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the Google App Engine
|
||||
service account.
|
||||
"""
|
||||
if self._service_account_email is None:
|
||||
self._service_account_email = (
|
||||
app_identity.get_service_account_name())
|
||||
return self._service_account_email
|
||||
|
||||
|
||||
class FlowProperty(db.Property):
|
||||
"""App Engine datastore Property for Flow.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = client.Flow
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
flow = super(FlowProperty, self).get_value_for_datastore(
|
||||
model_instance)
|
||||
return db.Blob(pickle.dumps(flow))
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return pickle.loads(value)
|
||||
|
||||
def validate(self, value):
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise db.BadValueError(
|
||||
'Property {0} must be convertible '
|
||||
'to a FlowThreeLegged instance ({1})'.format(self.name, value))
|
||||
return super(FlowProperty, self).validate(value)
|
||||
|
||||
def empty(self, value):
|
||||
return not value
|
||||
|
||||
|
||||
class CredentialsProperty(db.Property):
|
||||
"""App Engine datastore Property for Credentials.
|
||||
|
||||
Utility property that allows easy storage and retrieval of
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = client.Credentials
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
logger.info("get: Got type " + str(type(model_instance)))
|
||||
cred = super(CredentialsProperty, self).get_value_for_datastore(
|
||||
model_instance)
|
||||
if cred is None:
|
||||
cred = ''
|
||||
else:
|
||||
cred = cred.to_json()
|
||||
return db.Blob(cred)
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
logger.info("make: Got type " + str(type(value)))
|
||||
if value is None:
|
||||
return None
|
||||
if len(value) == 0:
|
||||
return None
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
def validate(self, value):
|
||||
value = super(CredentialsProperty, self).validate(value)
|
||||
logger.info("validate: Got type " + str(type(value)))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise db.BadValueError(
|
||||
'Property {0} must be convertible '
|
||||
'to a Credentials instance ({1})'.format(self.name, value))
|
||||
return value
|
||||
|
||||
|
||||
class StorageByKeyName(client.Storage):
|
||||
"""Store and retrieve a credential to and from the App Engine datastore.
|
||||
|
||||
This Storage helper presumes the Credentials have been stored as a
|
||||
CredentialsProperty or CredentialsNDBProperty on a datastore model class,
|
||||
and that entities are stored by key_name.
|
||||
"""
|
||||
|
||||
@_helpers.positional(4)
|
||||
def __init__(self, model, key_name, property_name, cache=None, user=None):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: db.Model or ndb.Model, model class
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
property_name: string, name of the property that is a
|
||||
CredentialsProperty or CredentialsNDBProperty.
|
||||
cache: memcache, a write-through cache to put in front of the
|
||||
datastore. If the model you are using is an NDB model, using
|
||||
a cache will be redundant since the model uses an instance
|
||||
cache and memcache for you.
|
||||
user: users.User object, optional. Can be used to grab user ID as a
|
||||
key_name if no key name is specified.
|
||||
"""
|
||||
super(StorageByKeyName, self).__init__()
|
||||
|
||||
if key_name is None:
|
||||
if user is None:
|
||||
raise ValueError('StorageByKeyName called with no '
|
||||
'key name or user.')
|
||||
key_name = user.user_id()
|
||||
|
||||
self._model = model
|
||||
self._key_name = key_name
|
||||
self._property_name = property_name
|
||||
self._cache = cache
|
||||
|
||||
def _is_ndb(self):
|
||||
"""Determine whether the model of the instance is an NDB model.
|
||||
|
||||
Returns:
|
||||
Boolean indicating whether or not the model is an NDB or DB model.
|
||||
"""
|
||||
# issubclass will fail if one of the arguments is not a class, only
|
||||
# need worry about new-style classes since ndb and db models are
|
||||
# new-style
|
||||
if isinstance(self._model, type):
|
||||
if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
|
||||
return True
|
||||
elif issubclass(self._model, db.Model):
|
||||
return False
|
||||
|
||||
raise TypeError(
|
||||
'Model class not an NDB or DB model: {0}.'.format(self._model))
|
||||
|
||||
def _get_entity(self):
|
||||
"""Retrieve entity from datastore.
|
||||
|
||||
Uses a different model method for db or ndb models.
|
||||
|
||||
Returns:
|
||||
Instance of the model corresponding to the current storage object
|
||||
and stored using the key name of the storage object.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
return self._model.get_by_id(self._key_name)
|
||||
else:
|
||||
return self._model.get_by_key_name(self._key_name)
|
||||
|
||||
def _delete_entity(self):
|
||||
"""Delete entity from datastore.
|
||||
|
||||
Attempts to delete using the key_name stored on the object, whether or
|
||||
not the given key is in the datastore.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
_NDB_KEY(self._model, self._key_name).delete()
|
||||
else:
|
||||
entity_key = db.Key.from_path(self._model.kind(), self._key_name)
|
||||
db.delete(entity_key)
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from datastore.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
if self._cache:
|
||||
json = self._cache.get(self._key_name)
|
||||
if json:
|
||||
credentials = client.Credentials.new_from_json(json)
|
||||
if credentials is None:
|
||||
entity = self._get_entity()
|
||||
if entity is not None:
|
||||
credentials = getattr(entity, self._property_name)
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
if credentials and hasattr(credentials, 'set_store'):
|
||||
credentials.set_store(self)
|
||||
return credentials
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_put(self, credentials):
|
||||
"""Write a Credentials to the datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
entity = self._model.get_or_insert(self._key_name)
|
||||
setattr(entity, self._property_name, credentials)
|
||||
entity.put()
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_delete(self):
|
||||
"""Delete Credential from datastore."""
|
||||
|
||||
if self._cache:
|
||||
self._cache.delete(self._key_name)
|
||||
|
||||
self._delete_entity()
|
||||
|
||||
|
||||
class CredentialsModel(db.Model):
|
||||
"""Storage for OAuth 2.0 Credentials
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsProperty()
|
||||
|
||||
|
||||
def _build_state_value(request_handler, user):
|
||||
"""Composes the value for the 'state' parameter.
|
||||
|
||||
Packs the current request URI and an XSRF token into an opaque string that
|
||||
can be passed to the authentication server via the 'state' parameter.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, The request.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The state value as a string.
|
||||
"""
|
||||
uri = request_handler.request.url
|
||||
token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
|
||||
action_id=str(uri))
|
||||
return uri + ':' + token
|
||||
|
||||
|
||||
def _parse_state_value(state, user):
|
||||
"""Parse the value of the 'state' parameter.
|
||||
|
||||
Parses the value and validates the XSRF token in the state parameter.
|
||||
|
||||
Args:
|
||||
state: string, The value of the state parameter.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The redirect URI, or None if XSRF token is not valid.
|
||||
"""
|
||||
uri, token = state.rsplit(':', 1)
|
||||
if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
|
||||
action_id=uri):
|
||||
return uri
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class OAuth2Decorator(object):
|
||||
"""Utility for making OAuth 2.0 easier.
|
||||
|
||||
Instantiate and then use with oauth_required or oauth_aware
|
||||
as decorators on webapp.RequestHandler methods.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2Decorator(
|
||||
client_id='837...ent.com',
|
||||
client_secret='Qh...wwI',
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be
|
||||
# used in API calls
|
||||
|
||||
"""
|
||||
|
||||
def set_credentials(self, credentials):
|
||||
self._tls.credentials = credentials
|
||||
|
||||
def get_credentials(self):
|
||||
"""A thread local Credentials object.
|
||||
|
||||
Returns:
|
||||
A client.Credentials object, or None if credentials hasn't been set
|
||||
in this thread yet, which may happen when calling has_credentials
|
||||
inside oauth_aware.
|
||||
"""
|
||||
return getattr(self._tls, 'credentials', None)
|
||||
|
||||
credentials = property(get_credentials, set_credentials)
|
||||
|
||||
def set_flow(self, flow):
|
||||
self._tls.flow = flow
|
||||
|
||||
def get_flow(self):
|
||||
"""A thread local Flow object.
|
||||
|
||||
Returns:
|
||||
A credentials.Flow object, or None if the flow hasn't been set in
|
||||
this thread yet, which happens in _create_flow() since Flows are
|
||||
created lazily.
|
||||
"""
|
||||
return getattr(self._tls, 'flow', None)
|
||||
|
||||
flow = property(get_flow, set_flow)
|
||||
|
||||
@_helpers.positional(4)
|
||||
def __init__(self, client_id, client_secret, scope,
|
||||
auth_uri=oauth2client.GOOGLE_AUTH_URI,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
user_agent=None,
|
||||
message=None,
|
||||
callback_path='/oauth2callback',
|
||||
token_response_param=None,
|
||||
_storage_class=StorageByKeyName,
|
||||
_credentials_class=CredentialsModel,
|
||||
_credentials_property_name='credentials',
|
||||
**kwargs):
|
||||
"""Constructor for OAuth2Decorator
|
||||
|
||||
Args:
|
||||
client_id: string, client identifier.
|
||||
client_secret: string client secret.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
auth_uri: string, URI for authorization endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider
|
||||
can be used.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
user_agent: string, User agent of your application, default to
|
||||
None.
|
||||
message: Message to display if there are problems with the
|
||||
OAuth 2.0 configuration. The message may contain HTML and
|
||||
will be presented on the web interface for any method that
|
||||
uses the decorator.
|
||||
callback_path: string, The absolute path to use as the callback
|
||||
URI. Note that this must match up with the URI given
|
||||
when registering the application in the APIs
|
||||
Console.
|
||||
token_response_param: string. If provided, the full JSON response
|
||||
to the access token request will be encoded
|
||||
and included in this query parameter in the
|
||||
callback URI. This is useful with providers
|
||||
(e.g. wordpress.com) that include extra
|
||||
fields that the client may want.
|
||||
_storage_class: "Protected" keyword argument not typically provided
|
||||
to this constructor. A storage class to aid in
|
||||
storing a Credentials object for a user in the
|
||||
datastore. Defaults to StorageByKeyName.
|
||||
_credentials_class: "Protected" keyword argument not typically
|
||||
provided to this constructor. A db or ndb Model
|
||||
class to hold credentials. Defaults to
|
||||
CredentialsModel.
|
||||
_credentials_property_name: "Protected" keyword argument not
|
||||
typically provided to this constructor.
|
||||
A string indicating the name of the
|
||||
field on the _credentials_class where a
|
||||
Credentials object will be stored.
|
||||
Defaults to 'credentials'.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
self._tls = threading.local()
|
||||
self.flow = None
|
||||
self.credentials = None
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._scope = _helpers.scopes_to_string(scope)
|
||||
self._auth_uri = auth_uri
|
||||
self._token_uri = token_uri
|
||||
self._revoke_uri = revoke_uri
|
||||
self._user_agent = user_agent
|
||||
self._kwargs = kwargs
|
||||
self._message = message
|
||||
self._in_error = False
|
||||
self._callback_path = callback_path
|
||||
self._token_response_param = token_response_param
|
||||
self._storage_class = _storage_class
|
||||
self._credentials_class = _credentials_class
|
||||
self._credentials_property_name = _credentials_property_name
|
||||
|
||||
def _display_error_message(self, request_handler):
|
||||
request_handler.response.out.write('<html><body>')
|
||||
request_handler.response.out.write(_safe_html(self._message))
|
||||
request_handler.response.out.write('</body></html>')
|
||||
|
||||
def oauth_required(self, method):
|
||||
"""Decorator that starts the OAuth 2.0 dance.
|
||||
|
||||
Starts the OAuth dance for the logged in user if they haven't already
|
||||
granted access for this application.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def check_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a
|
||||
# POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
# Store the request URI in 'state' so we can use it later
|
||||
self.flow.params['state'] = _build_state_value(
|
||||
request_handler, user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
|
||||
if not self.has_credentials():
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
except client.AccessTokenRefreshError:
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
|
||||
return check_oauth
|
||||
|
||||
def _create_flow(self, request_handler):
|
||||
"""Create the Flow object.
|
||||
|
||||
The Flow is calculated lazily since we don't know where this app is
|
||||
running until it receives a request, at which point redirect_uri can be
|
||||
calculated and then the Flow object can be constructed.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, the request handler.
|
||||
"""
|
||||
if self.flow is None:
|
||||
redirect_uri = request_handler.request.relative_url(
|
||||
self._callback_path) # Usually /oauth2callback
|
||||
self.flow = client.OAuth2WebServerFlow(
|
||||
self._client_id, self._client_secret, self._scope,
|
||||
redirect_uri=redirect_uri, user_agent=self._user_agent,
|
||||
auth_uri=self._auth_uri, token_uri=self._token_uri,
|
||||
revoke_uri=self._revoke_uri, **self._kwargs)
|
||||
|
||||
def oauth_aware(self, method):
|
||||
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
|
||||
|
||||
Does all the setup for the OAuth dance, but doesn't initiate it.
|
||||
This decorator is useful if you want to create a page that knows
|
||||
whether or not the user has granted access to this application.
|
||||
From within a method decorated with @oauth_aware the has_credentials()
|
||||
and authorize_url() methods can be called.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def setup_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a
|
||||
# POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
self.flow.params['state'] = _build_state_value(request_handler,
|
||||
user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
return setup_oauth
|
||||
|
||||
def has_credentials(self):
|
||||
"""True if for the logged in user there are valid access Credentials.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
return self.credentials is not None and not self.credentials.invalid
|
||||
|
||||
def authorize_url(self):
|
||||
"""Returns the URL to start the OAuth dance.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
url = self.flow.step1_get_authorize_url()
|
||||
return str(url)
|
||||
|
||||
def http(self, *args, **kwargs):
|
||||
"""Returns an authorized http instance.
|
||||
|
||||
Must only be called from within an @oauth_required decorated method, or
|
||||
from within an @oauth_aware decorated method where has_credentials()
|
||||
returns True.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to httplib2.Http constructor.
|
||||
**kwargs: Positional arguments passed to httplib2.Http constructor.
|
||||
"""
|
||||
return self.credentials.authorize(
|
||||
transport.get_http_object(*args, **kwargs))
|
||||
|
||||
@property
|
||||
def callback_path(self):
|
||||
"""The absolute path where the callback will occur.
|
||||
|
||||
Note this is the absolute path, not the absolute URI, that will be
|
||||
calculated by the decorator at runtime. See callback_handler() for how
|
||||
this should be used.
|
||||
|
||||
Returns:
|
||||
The callback path as a string.
|
||||
"""
|
||||
return self._callback_path
|
||||
|
||||
def callback_handler(self):
|
||||
"""RequestHandler for the OAuth 2.0 redirect callback.
|
||||
|
||||
Usage::
|
||||
|
||||
app = webapp.WSGIApplication([
|
||||
('/index', MyIndexHandler),
|
||||
...,
|
||||
(decorator.callback_path, decorator.callback_handler())
|
||||
])
|
||||
|
||||
Returns:
|
||||
A webapp.RequestHandler that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
decorator = self
|
||||
|
||||
class OAuth2Handler(webapp.RequestHandler):
|
||||
"""Handler for the redirect_uri of the OAuth 2.0 dance."""
|
||||
|
||||
@login_required
|
||||
def get(self):
|
||||
error = self.request.get('error')
|
||||
if error:
|
||||
errormsg = self.request.get('error_description', error)
|
||||
self.response.out.write(
|
||||
'The authorization request failed: {0}'.format(
|
||||
_safe_html(errormsg)))
|
||||
else:
|
||||
user = users.get_current_user()
|
||||
decorator._create_flow(self)
|
||||
credentials = decorator.flow.step2_exchange(
|
||||
self.request.params)
|
||||
decorator._storage_class(
|
||||
decorator._credentials_class, None,
|
||||
decorator._credentials_property_name,
|
||||
user=user).put(credentials)
|
||||
redirect_uri = _parse_state_value(
|
||||
str(self.request.get('state')), user)
|
||||
if redirect_uri is None:
|
||||
self.response.out.write(
|
||||
'The authorization request failed')
|
||||
return
|
||||
|
||||
if (decorator._token_response_param and
|
||||
credentials.token_response):
|
||||
resp_json = json.dumps(credentials.token_response)
|
||||
redirect_uri = _helpers._add_query_parameter(
|
||||
redirect_uri, decorator._token_response_param,
|
||||
resp_json)
|
||||
|
||||
self.redirect(redirect_uri)
|
||||
|
||||
return OAuth2Handler
|
||||
|
||||
def callback_application(self):
|
||||
"""WSGI application for handling the OAuth 2.0 redirect callback.
|
||||
|
||||
If you need finer grained control use `callback_handler` which returns
|
||||
just the webapp.RequestHandler.
|
||||
|
||||
Returns:
|
||||
A webapp.WSGIApplication that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
return webapp.WSGIApplication([
|
||||
(self.callback_path, self.callback_handler())
|
||||
])
|
||||
|
||||
|
||||
class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
|
||||
"""An OAuth2Decorator that builds from a clientsecrets file.
|
||||
|
||||
Uses a clientsecrets file as the source for all the information when
|
||||
constructing an OAuth2Decorator.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2DecoratorFromClientSecrets(
|
||||
os.path.join(os.path.dirname(__file__), 'client_secrets.json')
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be
|
||||
# used in API calls
|
||||
|
||||
"""
|
||||
|
||||
@_helpers.positional(3)
|
||||
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may
|
||||
contain HTML and will be presented on the web interface
|
||||
for any method that uses the decorator.
|
||||
cache: An optional cache service client that implements get() and
|
||||
set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename,
|
||||
cache=cache)
|
||||
if client_type not in (clientsecrets.TYPE_WEB,
|
||||
clientsecrets.TYPE_INSTALLED):
|
||||
raise clientsecrets.InvalidClientSecretsError(
|
||||
"OAuth2Decorator doesn't support this OAuth 2.0 flow.")
|
||||
|
||||
constructor_kwargs = dict(kwargs)
|
||||
constructor_kwargs.update({
|
||||
'auth_uri': client_info['auth_uri'],
|
||||
'token_uri': client_info['token_uri'],
|
||||
'message': message,
|
||||
})
|
||||
revoke_uri = client_info.get('revoke_uri')
|
||||
if revoke_uri is not None:
|
||||
constructor_kwargs['revoke_uri'] = revoke_uri
|
||||
super(OAuth2DecoratorFromClientSecrets, self).__init__(
|
||||
client_info['client_id'], client_info['client_secret'],
|
||||
scope, **constructor_kwargs)
|
||||
if message is not None:
|
||||
self._message = message
|
||||
else:
|
||||
self._message = 'Please configure your application for OAuth 2.0.'
|
||||
|
||||
|
||||
@_helpers.positional(2)
|
||||
def oauth2decorator_from_clientsecrets(filename, scope,
|
||||
message=None, cache=None):
|
||||
"""Creates an OAuth2Decorator populated from a clientsecrets file.
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or list of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may
|
||||
contain HTML and will be presented on the web interface for
|
||||
any method that uses the decorator.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
|
||||
Returns: An OAuth2Decorator
|
||||
"""
|
||||
return OAuth2DecoratorFromClientSecrets(filename, scope,
|
||||
message=message, cache=cache)
|
||||
@@ -1,152 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""OAuth 2.0 utitilies for Google Developer Shell environment."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
|
||||
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Errors for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationError(Error):
|
||||
"""Errors for communication with the Developer Shell server."""
|
||||
|
||||
|
||||
class NoDevshellServer(Error):
|
||||
"""Error when no Developer Shell server can be contacted."""
|
||||
|
||||
|
||||
# The request for credential information to the Developer Shell client socket
|
||||
# is always an empty PBLite-formatted JSON object, so just define it as a
|
||||
# constant.
|
||||
CREDENTIAL_INFO_REQUEST_JSON = '[]'
|
||||
|
||||
|
||||
class CredentialInfoResponse(object):
|
||||
"""Credential information response from Developer Shell server.
|
||||
|
||||
The credential information response from Developer Shell socket is a
|
||||
PBLite-formatted JSON array with fields encoded by their index in the
|
||||
array:
|
||||
|
||||
* Index 0 - user email
|
||||
* Index 1 - default project ID. None if the project context is not known.
|
||||
* Index 2 - OAuth2 access token. None if there is no valid auth context.
|
||||
* Index 3 - Seconds until the access token expires. None if not present.
|
||||
"""
|
||||
|
||||
def __init__(self, json_string):
|
||||
"""Initialize the response data from JSON PBLite array."""
|
||||
pbl = json.loads(json_string)
|
||||
if not isinstance(pbl, list):
|
||||
raise ValueError('Not a list: ' + str(pbl))
|
||||
pbl_len = len(pbl)
|
||||
self.user_email = pbl[0] if pbl_len > 0 else None
|
||||
self.project_id = pbl[1] if pbl_len > 1 else None
|
||||
self.access_token = pbl[2] if pbl_len > 2 else None
|
||||
self.expires_in = pbl[3] if pbl_len > 3 else None
|
||||
|
||||
|
||||
def _SendRecv():
|
||||
"""Communicate with the Developer Shell server socket."""
|
||||
|
||||
port = int(os.getenv(DEVSHELL_ENV, 0))
|
||||
if port == 0:
|
||||
raise NoDevshellServer()
|
||||
|
||||
sock = socket.socket()
|
||||
sock.connect(('localhost', port))
|
||||
|
||||
data = CREDENTIAL_INFO_REQUEST_JSON
|
||||
msg = '{0}\n{1}'.format(len(data), data)
|
||||
sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
|
||||
|
||||
header = sock.recv(6).decode()
|
||||
if '\n' not in header:
|
||||
raise CommunicationError('saw no newline in the first 6 bytes')
|
||||
len_str, json_str = header.split('\n', 1)
|
||||
to_read = int(len_str) - len(json_str)
|
||||
if to_read > 0:
|
||||
json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
|
||||
|
||||
return CredentialInfoResponse(json_str)
|
||||
|
||||
|
||||
class DevshellCredentials(client.GoogleCredentials):
|
||||
"""Credentials object for Google Developer Shell environment.
|
||||
|
||||
This object will allow a Google Developer Shell session to identify its
|
||||
user to Google and other OAuth 2.0 servers that can verify assertions. It
|
||||
can be used for the purpose of accessing data stored under the user
|
||||
account.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, user_agent=None):
|
||||
super(DevshellCredentials, self).__init__(
|
||||
None, # access_token, initialized below
|
||||
None, # client_id
|
||||
None, # client_secret
|
||||
None, # refresh_token
|
||||
None, # token_expiry
|
||||
None, # token_uri
|
||||
user_agent)
|
||||
self._refresh(None)
|
||||
|
||||
def _refresh(self, http):
|
||||
"""Refreshes the access token.
|
||||
|
||||
Args:
|
||||
http: unused HTTP object
|
||||
"""
|
||||
self.devshell_response = _SendRecv()
|
||||
self.access_token = self.devshell_response.access_token
|
||||
expires_in = self.devshell_response.expires_in
|
||||
if expires_in is not None:
|
||||
delta = datetime.timedelta(seconds=expires_in)
|
||||
self.token_expiry = client._UTCNOW() + delta
|
||||
else:
|
||||
self.token_expiry = None
|
||||
|
||||
@property
|
||||
def user_email(self):
|
||||
return self.devshell_response.user_email
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.devshell_response.project_id
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
raise NotImplementedError(
|
||||
'Cannot load Developer Shell credentials from JSON.')
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize Developer Shell credentials.')
|
||||
@@ -1,65 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Dictionary storage for OAuth2 Credentials."""
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class DictionaryStorage(client.Storage):
|
||||
"""Store and retrieve credentials to and from a dictionary-like object.
|
||||
|
||||
Args:
|
||||
dictionary: A dictionary or dictionary-like object.
|
||||
key: A string or other hashable. The credentials will be stored in
|
||||
``dictionary[key]``.
|
||||
lock: An optional threading.Lock-like object. The lock will be
|
||||
acquired before anything is written or read from the
|
||||
dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, dictionary, key, lock=None):
|
||||
"""Construct a DictionaryStorage instance."""
|
||||
super(DictionaryStorage, self).__init__(lock=lock)
|
||||
self._dictionary = dictionary
|
||||
self._key = key
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve the credentials from the dictionary, if they exist.
|
||||
|
||||
Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
|
||||
"""
|
||||
serialized = self._dictionary.get(self._key)
|
||||
|
||||
if serialized is None:
|
||||
return None
|
||||
|
||||
credentials = client.OAuth2Credentials.from_json(serialized)
|
||||
credentials.set_store(self)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Save the credentials to the dictionary.
|
||||
|
||||
Args:
|
||||
credentials: A :class:`oauth2client.client.OAuth2Credentials`
|
||||
instance.
|
||||
"""
|
||||
serialized = credentials.to_json()
|
||||
self._dictionary[self._key] = serialized
|
||||
|
||||
def locked_delete(self):
|
||||
"""Remove the credentials from the dictionary, if they exist."""
|
||||
self._dictionary.pop(self._key, None)
|
||||
@@ -1,489 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Utilities for the Django web framework.
|
||||
|
||||
Provides Django views and helpers the make using the OAuth2 web server
|
||||
flow easier. It includes an ``oauth_required`` decorator to automatically
|
||||
ensure that user credentials are available, and an ``oauth_enabled`` decorator
|
||||
to check if the user has authorized, and helper shortcuts to create the
|
||||
authorization URL otherwise.
|
||||
|
||||
There are two basic use cases supported. The first is using Google OAuth as the
|
||||
primary form of authentication, which is the simpler approach recommended
|
||||
for applications without their own user system.
|
||||
|
||||
The second use case is adding Google OAuth credentials to an
|
||||
existing Django model containing a Django user field. Most of the
|
||||
configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in
|
||||
settings.py. See "Adding Credentials To An Existing Django User System" for
|
||||
usage differences.
|
||||
|
||||
Only Django versions 1.8+ are supported.
|
||||
|
||||
Configuration
|
||||
===============
|
||||
|
||||
To configure, you'll need a set of OAuth2 web application credentials from
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
|
||||
|
||||
Add the helper to your INSTALLED_APPS:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: installed_apps
|
||||
|
||||
INSTALLED_APPS = (
|
||||
# other apps
|
||||
"django.contrib.sessions.middleware"
|
||||
"oauth2client.contrib.django_util"
|
||||
)
|
||||
|
||||
This helper also requires the Django Session Middleware, so
|
||||
``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
|
||||
MIDDLEWARE or MIDDLEWARE_CLASSES (in Django versions <1.10) should also
|
||||
contain the string 'django.contrib.sessions.middleware.SessionMiddleware'.
|
||||
|
||||
|
||||
Add the client secrets created earlier to the settings. You can either
|
||||
specify the path to the credentials file in JSON format
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_file
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
|
||||
|
||||
Or, directly configure the client Id and client secret.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_config
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_ID=client-id-field
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
|
||||
|
||||
By default, the default scopes for the required decorator only contains the
|
||||
``email`` scopes. You can change that default in the settings.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: scopes
|
||||
|
||||
GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
|
||||
|
||||
By default, the decorators will add an `oauth` object to the Django request
|
||||
object, and include all of its state and helpers inside that object. If the
|
||||
`oauth` name conflicts with another usage, it can be changed
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: request_prefix
|
||||
|
||||
# changes request.oauth to request.google_oauth
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
|
||||
|
||||
Add the oauth2 routes to your application's urls.py urlpatterns.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: urls.py
|
||||
:name: urls
|
||||
|
||||
from oauth2client.contrib.django_util.site import urls as oauth2_urls
|
||||
|
||||
urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
|
||||
|
||||
To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
|
||||
This creates a credentials object with an id_token, and allows you to create
|
||||
an `http` object to build service clients with. These are all attached to the
|
||||
request.oauth
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.oauth.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(calendarId='primary').execute()['items']
|
||||
return HttpResponse("email: {0} , calendar: {1}".format(
|
||||
email,str(events)))
|
||||
return HttpResponse(
|
||||
"email: {0} , calendar: {1}".format(email, str(events)))
|
||||
|
||||
To make OAuth2 optional and provide an authorization link in your own views.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled2
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: {0}".format(
|
||||
request.oauth.credentials.id_token['email']))
|
||||
else:
|
||||
return HttpResponse(
|
||||
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
|
||||
'</a>'.format(request.oauth.get_authorize_redirect()))
|
||||
|
||||
If a view needs a scope not included in the default scopes specified in
|
||||
the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
|
||||
and specify additional scopes in the decorator arguments.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_additional_scopes
|
||||
|
||||
@oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
|
||||
def drive_required(request):
|
||||
if request.oauth.has_credentials():
|
||||
service = build(serviceName='drive', version='v2',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.files().list().execute()['items']
|
||||
return HttpResponse(str(events))
|
||||
else:
|
||||
return HttpResponse(
|
||||
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
|
||||
'</a>'.format(request.oauth.get_authorize_redirect()))
|
||||
|
||||
|
||||
To provide a callback on authorization being completed, use the
|
||||
oauth2_authorized signal:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: signals
|
||||
|
||||
from oauth2client.contrib.django_util.signals import oauth2_authorized
|
||||
|
||||
def test_callback(sender, request, credentials, **kwargs):
|
||||
print("Authorization Signal Received {0}".format(
|
||||
credentials.id_token['email']))
|
||||
|
||||
oauth2_authorized.connect(test_callback)
|
||||
|
||||
Adding Credentials To An Existing Django User System
|
||||
=====================================================
|
||||
|
||||
As an alternative to storing the credentials in the session, the helper
|
||||
can be configured to store the fields on a Django model. This might be useful
|
||||
if you need to use the credentials outside the context of a user request. It
|
||||
also prevents the need for a logged in user to repeat the OAuth flow when
|
||||
starting a new session.
|
||||
|
||||
To use, change ``settings.py``
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: storage_model_config
|
||||
|
||||
GOOGLE_OAUTH2_STORAGE_MODEL = {
|
||||
'model': 'path.to.model.MyModel',
|
||||
'user_property': 'user_id',
|
||||
'credentials_property': 'credential'
|
||||
}
|
||||
|
||||
Where ``path.to.model`` class is the fully qualified name of a
|
||||
``django.db.model`` class containing a ``django.contrib.auth.models.User``
|
||||
field with the name specified by `user_property` and a
|
||||
:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name
|
||||
specified by `credentials_property`. For the sample configuration given,
|
||||
our model would look like
|
||||
|
||||
.. code-block:: python
|
||||
:caption: models.py
|
||||
:name: storage_model_model
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from oauth2client.contrib.django_util.models import CredentialsField
|
||||
|
||||
class MyModel(models.Model):
|
||||
# ... other fields here ...
|
||||
user = models.OneToOneField(User)
|
||||
credential = CredentialsField()
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
||||
import django.conf
|
||||
from django.core import exceptions
|
||||
from django.core import urlresolvers
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import transport
|
||||
from oauth2client.contrib import dictionary_storage
|
||||
from oauth2client.contrib.django_util import storage
|
||||
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
|
||||
|
||||
|
||||
def _load_client_secrets(filename):
|
||||
"""Loads client secrets from the given filename.
|
||||
|
||||
Args:
|
||||
filename: The name of the file containing the JSON secret key.
|
||||
|
||||
Returns:
|
||||
A 2-tuple, the first item containing the client id, and the second
|
||||
item containing a client secret.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in {} is not supported, only the WEB flow '
|
||||
'type is supported.'.format(client_type))
|
||||
return client_info['client_id'], client_info['client_secret']
|
||||
|
||||
|
||||
def _get_oauth2_client_id_and_secret(settings_instance):
|
||||
"""Initializes client id and client secret based on the settings.
|
||||
|
||||
Args:
|
||||
settings_instance: An instance of ``django.conf.settings``.
|
||||
|
||||
Returns:
|
||||
A 2-tuple, the first item is the client id and the second
|
||||
item is the client secret.
|
||||
"""
|
||||
secret_json = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
|
||||
if secret_json is not None:
|
||||
return _load_client_secrets(secret_json)
|
||||
else:
|
||||
client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
|
||||
None)
|
||||
client_secret = getattr(settings_instance,
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET", None)
|
||||
if client_id is not None and client_secret is not None:
|
||||
return client_id, client_secret
|
||||
else:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
|
||||
"both GOOGLE_OAUTH2_CLIENT_ID and "
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET in settings.py")
|
||||
|
||||
|
||||
def _get_storage_model():
|
||||
"""This configures whether the credentials will be stored in the session
|
||||
or the Django ORM based on the settings. By default, the credentials
|
||||
will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL`
|
||||
is found in the settings. Usually, the ORM storage is used to integrate
|
||||
credentials into an existing Django user system.
|
||||
|
||||
Returns:
|
||||
A tuple containing three strings, or None. If
|
||||
``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple
|
||||
will contain the fully qualifed path of the `django.db.model`,
|
||||
the name of the ``django.contrib.auth.models.User`` field on the
|
||||
model, and the name of the
|
||||
:class:`oauth2client.contrib.django_util.models.CredentialsField`
|
||||
field on the model. If Django ORM storage is not configured,
|
||||
this function returns None.
|
||||
"""
|
||||
storage_model_settings = getattr(django.conf.settings,
|
||||
'GOOGLE_OAUTH2_STORAGE_MODEL', None)
|
||||
if storage_model_settings is not None:
|
||||
return (storage_model_settings['model'],
|
||||
storage_model_settings['user_property'],
|
||||
storage_model_settings['credentials_property'])
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
class OAuth2Settings(object):
|
||||
"""Initializes Django OAuth2 Helper Settings
|
||||
|
||||
This class loads the OAuth2 Settings from the Django settings, and then
|
||||
provides those settings as attributes to the rest of the views and
|
||||
decorators in the module.
|
||||
|
||||
Attributes:
|
||||
scopes: A list of OAuth2 scopes that the decorators and views will use
|
||||
as defaults.
|
||||
request_prefix: The name of the attribute that the decorators use to
|
||||
attach the UserOAuth2 object to the Django request object.
|
||||
client_id: The OAuth2 Client ID.
|
||||
client_secret: The OAuth2 Client Secret.
|
||||
"""
|
||||
|
||||
def __init__(self, settings_instance):
|
||||
self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES)
|
||||
self.request_prefix = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
|
||||
info = _get_oauth2_client_id_and_secret(settings_instance)
|
||||
self.client_id, self.client_secret = info
|
||||
|
||||
# Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE
|
||||
middleware_settings = getattr(settings_instance, 'MIDDLEWARE', None)
|
||||
if middleware_settings is None:
|
||||
middleware_settings = getattr(
|
||||
settings_instance, 'MIDDLEWARE_CLASSES', None)
|
||||
if middleware_settings is None:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'Django settings has neither MIDDLEWARE nor MIDDLEWARE_CLASSES'
|
||||
'configured')
|
||||
|
||||
if ('django.contrib.sessions.middleware.SessionMiddleware' not in
|
||||
middleware_settings):
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'The Google OAuth2 Helper requires session middleware to '
|
||||
'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE '
|
||||
'setting to include \'django.contrib.sessions.middleware.'
|
||||
'SessionMiddleware\'.')
|
||||
(self.storage_model, self.storage_model_user_property,
|
||||
self.storage_model_credentials_property) = _get_storage_model()
|
||||
|
||||
|
||||
oauth2_settings = OAuth2Settings(django.conf.settings)
|
||||
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
|
||||
|
||||
def get_storage(request):
|
||||
""" Gets a Credentials storage object provided by the Django OAuth2 Helper
|
||||
object.
|
||||
|
||||
Args:
|
||||
request: Reference to the current request object.
|
||||
|
||||
Returns:
|
||||
An :class:`oauth2.client.Storage` object.
|
||||
"""
|
||||
storage_model = oauth2_settings.storage_model
|
||||
user_property = oauth2_settings.storage_model_user_property
|
||||
credentials_property = oauth2_settings.storage_model_credentials_property
|
||||
|
||||
if storage_model:
|
||||
module_name, class_name = storage_model.rsplit('.', 1)
|
||||
module = importlib.import_module(module_name)
|
||||
storage_model_class = getattr(module, class_name)
|
||||
return storage.DjangoORMStorage(storage_model_class,
|
||||
user_property,
|
||||
request.user,
|
||||
credentials_property)
|
||||
else:
|
||||
# use session
|
||||
return dictionary_storage.DictionaryStorage(
|
||||
request.session, key=_CREDENTIALS_KEY)
|
||||
|
||||
|
||||
def _redirect_with_params(url_name, *args, **kwargs):
|
||||
"""Helper method to create a redirect response with URL params.
|
||||
|
||||
This builds a redirect string that converts kwargs into a
|
||||
query string.
|
||||
|
||||
Args:
|
||||
url_name: The name of the url to redirect to.
|
||||
kwargs: the query string param and their values to build.
|
||||
|
||||
Returns:
|
||||
A properly formatted redirect string.
|
||||
"""
|
||||
url = urlresolvers.reverse(url_name, args=args)
|
||||
params = parse.urlencode(kwargs, True)
|
||||
return "{0}?{1}".format(url, params)
|
||||
|
||||
|
||||
def _credentials_from_request(request):
|
||||
"""Gets the authorized credentials for this flow, if they exist."""
|
||||
# ORM storage requires a logged in user
|
||||
if (oauth2_settings.storage_model is None or
|
||||
request.user.is_authenticated()):
|
||||
return get_storage(request).get()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
"""Class to create oauth2 objects on Django request objects containing
|
||||
credentials and helper methods.
|
||||
"""
|
||||
|
||||
def __init__(self, request, scopes=None, return_url=None):
|
||||
"""Initialize the Oauth2 Object.
|
||||
|
||||
Args:
|
||||
request: Django request object.
|
||||
scopes: Scopes desired for this OAuth2 flow.
|
||||
return_url: The url to return to after the OAuth flow is complete,
|
||||
defaults to the request's current URL path.
|
||||
"""
|
||||
self.request = request
|
||||
self.return_url = return_url or request.get_full_path()
|
||||
if scopes:
|
||||
self._scopes = set(oauth2_settings.scopes) | set(scopes)
|
||||
else:
|
||||
self._scopes = set(oauth2_settings.scopes)
|
||||
|
||||
def get_authorize_redirect(self):
|
||||
"""Creates a URl to start the OAuth2 authorization flow."""
|
||||
get_params = {
|
||||
'return_url': self.return_url,
|
||||
'scopes': self._get_scopes()
|
||||
}
|
||||
|
||||
return _redirect_with_params('google_oauth:authorize', **get_params)
|
||||
|
||||
def has_credentials(self):
|
||||
"""Returns True if there are valid credentials for the current user
|
||||
and required scopes."""
|
||||
credentials = _credentials_from_request(self.request)
|
||||
return (credentials and not credentials.invalid and
|
||||
credentials.has_scopes(self._get_scopes()))
|
||||
|
||||
def _get_scopes(self):
|
||||
"""Returns the scopes associated with this object, kept up to
|
||||
date for incremental auth."""
|
||||
if _credentials_from_request(self.request):
|
||||
return (self._scopes |
|
||||
_credentials_from_request(self.request).scopes)
|
||||
else:
|
||||
return self._scopes
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""Returns the scopes associated with this OAuth2 object."""
|
||||
# make sure previously requested custom scopes are maintained
|
||||
# in future authorizations
|
||||
return self._get_scopes()
|
||||
|
||||
@property
|
||||
def credentials(self):
|
||||
"""Gets the authorized credentials for this flow, if they exist."""
|
||||
return _credentials_from_request(self.request)
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
"""Helper: create HTTP client authorized with OAuth2 credentials."""
|
||||
if self.has_credentials():
|
||||
return self.credentials.authorize(transport.get_http_object())
|
||||
return None
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Application Config For Django OAuth2 Helper.
|
||||
|
||||
Django 1.7+ provides an
|
||||
[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
|
||||
API so that Django projects can introspect on installed applications using a
|
||||
stable API. This module exists to follow that convention.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Django 1.7+ only supports Python 2.7+
|
||||
if sys.hexversion >= 0x02070000: # pragma: NO COVER
|
||||
from django.apps import AppConfig
|
||||
|
||||
class GoogleOAuth2HelperConfig(AppConfig):
|
||||
""" App Config for Django Helper"""
|
||||
name = 'oauth2client.django_util'
|
||||
verbose_name = "Google OAuth2 Django Helper"
|
||||
@@ -1,145 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Decorators for Django OAuth2 Flow.
|
||||
|
||||
Contains two decorators, ``oauth_required`` and ``oauth_enabled``.
|
||||
|
||||
``oauth_required`` will ensure that a user has an oauth object containing
|
||||
credentials associated with the request, and if not, redirect to the
|
||||
authorization flow.
|
||||
|
||||
``oauth_enabled`` will attach the oauth2 object containing credentials if it
|
||||
exists. If it doesn't, the view will still render, but helper methods will be
|
||||
attached to start the oauth2 flow.
|
||||
"""
|
||||
|
||||
from django import shortcuts
|
||||
import django.conf
|
||||
from six import wraps
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client.contrib import django_util
|
||||
|
||||
|
||||
def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to require OAuth2 credentials for a view.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_2
|
||||
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(
|
||||
calendarId='primary').execute()['items']
|
||||
return HttpResponse(
|
||||
"email: {0}, calendar: {1}".format(email, str(events)))
|
||||
|
||||
Args:
|
||||
decorated_function: View function to decorate, must have the Django
|
||||
request object as the first argument.
|
||||
scopes: Scopes to require, will default.
|
||||
decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete.
|
||||
|
||||
Returns:
|
||||
An OAuth2 Authorize view if credentials are not found or if the
|
||||
credentials are missing the required scopes. Otherwise,
|
||||
the decorated view.
|
||||
"""
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(request, *args, **kwargs):
|
||||
if not (django_util.oauth2_settings.storage_model is None or
|
||||
request.user.is_authenticated()):
|
||||
redirect_str = '{0}?next={1}'.format(
|
||||
django.conf.settings.LOGIN_URL,
|
||||
parse.quote(request.path))
|
||||
return shortcuts.redirect(redirect_str)
|
||||
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
if not user_oauth.has_credentials():
|
||||
return shortcuts.redirect(user_oauth.get_authorize_redirect())
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
|
||||
|
||||
def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to enable OAuth Credentials if authorized, and setup
|
||||
the oauth object on the request object to provide helper functions
|
||||
to start the flow otherwise.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled3
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: {0}".format(
|
||||
request.oauth.credentials.id_token['email'])
|
||||
else:
|
||||
return HttpResponse('Here is an OAuth Authorize link:
|
||||
<a href="{0}">Authorize</a>'.format(
|
||||
request.oauth.get_authorize_redirect()))
|
||||
|
||||
|
||||
Args:
|
||||
decorated_function: View function to decorate.
|
||||
scopes: Scopes to require, will default.
|
||||
decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete.
|
||||
|
||||
Returns:
|
||||
The decorated view function.
|
||||
"""
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def enabled_wrapper(request, *args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return enabled_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
@@ -1,82 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Contains classes used for the Django ORM storage."""
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
from django.db import models
|
||||
from django.utils import encoding
|
||||
import jsonpickle
|
||||
|
||||
import oauth2client
|
||||
|
||||
|
||||
class CredentialsField(models.Field):
|
||||
"""Django ORM field for storing OAuth2 Credentials."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
kwargs['null'] = True
|
||||
super(CredentialsField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'BinaryField'
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
"""Overrides ``models.Field`` method. This converts the value
|
||||
returned from the database to an instance of this class.
|
||||
"""
|
||||
return self.to_python(value)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Overrides ``models.Field`` method. This is used to convert
|
||||
bytes (from serialization etc) to an instance of this class"""
|
||||
if value is None:
|
||||
return None
|
||||
elif isinstance(value, oauth2client.client.Credentials):
|
||||
return value
|
||||
else:
|
||||
try:
|
||||
return jsonpickle.decode(
|
||||
base64.b64decode(encoding.smart_bytes(value)).decode())
|
||||
except ValueError:
|
||||
return pickle.loads(
|
||||
base64.b64decode(encoding.smart_bytes(value)))
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"""Overrides ``models.Field`` method. This is used to convert
|
||||
the value from an instances of this class to bytes that can be
|
||||
inserted into the database.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
else:
|
||||
return encoding.smart_text(
|
||||
base64.b64encode(jsonpickle.encode(value).encode()))
|
||||
|
||||
def value_to_string(self, obj):
|
||||
"""Convert the field value from the provided model to a string.
|
||||
|
||||
Used during model serialization.
|
||||
|
||||
Args:
|
||||
obj: db.Model, model object
|
||||
|
||||
Returns:
|
||||
string, the serialized field value
|
||||
"""
|
||||
value = self._get_val_from_obj(obj)
|
||||
return self.get_prep_value(value)
|
||||
@@ -1,28 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Signals for Google OAuth2 Helper.
|
||||
|
||||
This module contains signals for Google OAuth2 Helper. Currently it only
|
||||
contains one, which fires when an OAuth2 authorization flow has completed.
|
||||
"""
|
||||
|
||||
import django.dispatch
|
||||
|
||||
"""Signal that fires when OAuth2 Flow has completed.
|
||||
It passes the Django request object and the OAuth2 credentials object to the
|
||||
receiver.
|
||||
"""
|
||||
oauth2_authorized = django.dispatch.Signal(
|
||||
providing_args=["request", "credentials"])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user