Compare commits

...

63 Commits
v4.03 ... v4.1

Author SHA1 Message Date
Ross Scroggs
c76368509e rint start_time and end_time as dates in gam reports; handle bad data from Google (#397) 2017-01-24 14:51:43 -05:00
Jay Lee
b54eb97f6b Use profile instead of email scope since it's 1 less scope ultimately 2017-01-24 14:50:26 -05:00
Jay Lee
2d997fb046 silence noisy oauth2client helpers 2017-01-24 14:49:37 -05:00
Jay Lee
e2cf769b20 short URLs fix back 2017-01-24 14:48:44 -05:00
Jay Lee
281786b3b9 prettify oauth2.txt 2017-01-24 14:04:56 -05:00
Jay Lee
b06b8608d0 googleapiclient 1.6.1 2017-01-24 14:03:03 -05:00
Jay Lee
22dc39eb85 Merge branch 'master' of https://github.com/jay0lee/GAM 2017-01-24 14:00:32 -05:00
Jay Lee
4a894958f0 oauth2client 4.0 2017-01-24 14:00:07 -05:00
Ross Scroggs
38273a786a Supply missing imports in utils.py (#396) 2017-01-24 13:47:56 -05:00
Jay Lee
6ba0a5d942 4.1 whatsnew.txt 2017-01-24 13:20:05 -05:00
Jay Lee
2ad731f4e0 GAM 4.1 2017-01-24 13:15:16 -05:00
Jay Lee
a491fb5471 Update ToS page for projects 2017-01-24 13:14:45 -05:00
Jay Lee
33cfb940b4 remove accidental break 2017-01-24 12:17:52 -05:00
Jay Lee
46b79334e4 pull APIs from GitHub master, import urllib2 once 2017-01-24 12:15:11 -05:00
Jay Lee
a7e841bcba put APIs to enable for project in own file to pull live 2017-01-24 12:00:17 -05:00
Jay Lee
f2400b35b0 Update API list for create project 2017-01-16 15:29:25 -05:00
Ross Scroggs
31058336ce Fix typo, clean up imports (#381)
* Fix typo, utils.py needs a few globals

* Clean up imports
2017-01-04 20:23:53 -05:00
Ross Scroggs
ac2cbef7f8 Fix gam transfer drive to properly handle orphaned files (#380) 2017-01-04 19:19:49 -05:00
Ross Scroggs
d7187ff998 When default SKU/Product list is used, sort it so output is cleaner (#379) 2017-01-04 17:57:23 -05:00
Ross Scroggs
2cc79f44ea Update licenses (#376)
* Update documentation for new license aliases

Add government as in PR #362

* Allow full SKU to be specified
2016-12-31 11:50:39 -05:00
Jay Lee
067a67c14e Update README.md 2016-12-31 11:32:40 -05:00
Ross Scroggs
054addfa9b Fix typo in documentation (#375) 2016-12-31 10:42:43 -05:00
Jay Lee
6b7cf875de utils.py for simple util functions 2016-12-29 16:32:22 -05:00
Jay Lee
581e31499b move more vars to var.py 2016-12-29 15:44:33 -05:00
Jay Lee
a5883a8429 consolidate license info 2016-12-29 14:31:54 -05:00
Jay Lee
95c2d91a5b pull variables into their own file 2016-12-29 13:55:14 -05:00
Ross Scroggs
9f487d57fb Get just the field we need in info user licenses (#374) 2016-12-27 18:28:21 -05:00
Ross Scroggs
e0d278e7ea Added count and allfields arguments to gam print groups (#373) 2016-12-27 15:30:09 -05:00
Ross Scroggs
36725c3574 Fix typo, update documentation (#372) 2016-12-27 12:44:50 -05:00
Ross Scroggs
8c911215b1 Site Verification API should be included in project APIs (#371) 2016-12-27 12:37:35 -05:00
Ross Scroggs
2b57a976c2 Clean up new batch licensing (#370) 2016-12-27 11:49:37 -05:00
Ross Scroggs
4817ce282a Handle suspended users with calendar commands; improve show calsettings output (#369) 2016-12-27 11:34:45 -05:00
Ross Scroggs
2da6666587 Add directmemberscount to list of group field names (#368) 2016-12-27 11:00:52 -05:00
Jay Lee
07f1bc050a Improve user info response with license batching 2016-12-27 10:57:18 -05:00
Ross Scroggs
9135e15b12 Fix error handling for nonexistent users in data transfers (#367) 2016-12-27 10:31:26 -05:00
Ross Scroggs
5c32a86257 Use patch instead of update in doUpdateCustomer, update is broken (#366) 2016-12-27 10:08:31 -05:00
Ross Scroggs
d5bcac4d14 Fix run-batch to handle 0 items (#363) 2016-12-21 15:04:30 -05:00
Ross Scroggs
73bcadbbfe Handle Google-Apps-For-Government (#362) 2016-12-21 14:05:59 -05:00
Jay Lee
d3091d3dd8 Update README.md 2016-11-20 15:39:46 -05:00
Jay Lee
f9ab78e393 update some strings to G Suite 2016-11-20 14:18:28 -05:00
Jay Lee
7a94077906 Update README.md 2016-11-20 13:58:36 -05:00
Jay Lee
6b438c3a46 Update README.md 2016-11-20 13:57:45 -05:00
Jay Lee
5383ed22ea Update README.md 2016-11-20 13:53:22 -05:00
Ross Scroggs
dbd09daa33 Fix Windows multiprocessing (#346)
* Fix Windows multiprocessing

* Clean up Windows initial setup
2016-11-19 13:23:33 -05:00
Ross Scroggs
25ace13a3d Handle additional run_batch calls (#344) 2016-11-19 07:16:23 -05:00
Ross Scroggs
8fc6112e32 Handle folder alternateLink (#343) 2016-11-18 19:36:54 -05:00
Ross Scroggs
ac2eb99d63 Handle Google mis-reporting invalid group in gam print groups settings (#342) 2016-11-18 18:32:55 -05:00
Ross Scroggs
43707d8074 Create missing label with gam add filter (#340)
Restores behavior present with Email Settings API
2016-11-18 17:33:43 -05:00
Jay Lee
b0b3c18e99 use multiprocessing instead of threading to take advantage of multi CPU systems 2016-11-18 15:45:57 -05:00
Jay Lee
349f2801c5 Improve batch performance
* Use a single GAM / Python process for all threads (needs testing, will sys.exit in a function cause issues?)
    - Huge reduction in useless time spent starting Python per-thread.
  * Bump default from 5 threads to 25.
  * Introduce default_to_batch for some user commands where it makes sense.
    - most show/print commands will default to batch off.
    - most do / update commands will default to on.
2016-11-18 10:19:19 -05:00
Ross Scroggs
658e7beb2b Standardize handling oauth2.txt (#337) 2016-11-17 11:57:10 -05:00
Ross Scroggs
e777eb6c99 Handle Google reporting invalid when getting group settings (#335) 2016-11-16 12:45:04 -05:00
Ross Scroggs
8a6ce43ad3 Do not get settings for special groups abuse and postmaster (#334)
* In gam print groups settings, get gs service outside of loop

* Do not get settings for special groups abuse and postmaster

* Do not set settings for special groups abuse and postmaster
2016-11-15 22:36:59 -05:00
Jay Lee
72d182032b point to create project / check serviceaccount rather than Wiki 2016-11-15 13:25:54 -05:00
Ross Scroggs
9b8189a3e4 In gam print groups settings, get gs service outside of loop (#333) 2016-11-15 09:21:05 -05:00
Jay Lee
14eb9ca3f8 Cleanup signature/vacation processing (#332) 2016-11-14 16:22:58 -05:00
Ross Scroggs
cc2cba8c70 Cleanup signature/vacation processing 2016-11-14 13:16:07 -08:00
Ross Scroggs
17660220fe Added html argument to gam add sendas/update sendas/signature to control newline (NL) processing when signature is read from a file. (#331) 2016-11-13 15:27:15 -05:00
Ross Scroggs
d5538a79da Update NL handling in doSignature and doVacation (#330) 2016-11-13 13:48:32 -05:00
Ross Scroggs
fc5cd1c219 Improve Unicode handling when reading files. (#329) 2016-11-13 13:02:00 -05:00
Ross Scroggs
e10e63a87f Cleanup doDelProjects/doCreateProject (#327)
Move common code to get CRM service into separate routine
Have delete projects get login_hint like oath create and create project
2016-11-12 16:09:17 -05:00
Ross Scroggs
93b05de15e Update build scripts (#326) 2016-11-12 13:07:19 -05:00
Ross Scroggs
a6a94060c6 Standardize getting login_hint for create project and oauth request (#325)
* Use RE to validate login_hint email address

http://stackoverflow.com/questions/201323/using-a-regular-expression-to-
validate-an-email-address

Per the W3C HTML5 spec:

* Use simpler RE pattern for email validation
2016-11-11 11:26:17 -05:00
32 changed files with 2041 additions and 1444 deletions

View File

@@ -1,22 +1,19 @@
GAM
============================
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily.
Downloads
---------
You can download the current GAM release from the [GitHub Releases] page.
Documentation
------------------
# Quick Start
## Linux / MacOS
Open a terminal and run:
```
bash <(curl -s -S -L https://git.io/install-gam)
```
this will download GAM, install it and start setup.
## Windows
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
# Documentation
The GAM documentation is hosted in the [GitHub Wiki]
Mailing List / Discussion group
-------------------------------
# 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.
Author
------
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>.
# Author
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to the mailing list.
[GAM release]: https://git.io/gamreleases
[GitHub Releases]: https://github.com/jay0lee/GAM/releases

View File

@@ -8,7 +8,7 @@ If an item contains spaces, it should be surrounded by " or '.
+ item may appear one or more times
| separates alternative items
# Primatives
Primitives
<Digit> ::= 0|1|2|3|4|5|6|7|8|9
<Number> ::= <Digit>+
<Hex> ::= <Digit>|a|b|c|d|e|f|A|B|C|D|E|F
@@ -17,15 +17,22 @@ If an item contains spaces, it should be surrounded by " or '.
<TrueValues> ::= true|on|yes|enabled|1
<FalseValues>= false|off|no|disabled|0
<DataTransferService> ::= googledrive|gdrive|drive|"drive and docs"
<ProductID> ::= Google-Apps|Google-Coordinate|Google-Drive-storage|Google-Vault
<SKUID> ::= apps|gafb|gafw|gams|gau|unlimited|d4w|dfw|coordinate|vault|vfe|
<ProductID> ::= Google-Apps|Google-Drive-storage|Google-Vault|Google-Coordinate
<SKUID> ::= gafb|gafw|basic|gsuite-basic|
gafg|gsuite-government|
gal|lite|gsuite-lite|
gams|postini|gsuite-gams|
gau|unlimited|gsuite-business|
drive-20gb|drive20gb|20gb|drive-50gb|drive50gb|50gb|drive-200gb|drive200gb|200gb|drive-400gb|drive400gb|400gb|
drive-1tb|drive1tb|1tb|drive-2tb|drive2tb|2tb|drive-4tb|drive4tb|4tb|drive-8tb|drive8tb|8tb|drive-16tb|drive16tb|16tb
drive-1tb|drive1tb|1tb|drive-2tb|drive2tb|2tb|drive-4tb|drive4tb|4tb|drive-8tb|drive8tb|8tb|drive-16tb|drive16tb|16tb|
vault|
vfe|
coordinate
<Charset> ::= ascii|mbcs|utf-8|utf-8-sig|utf-16|<String>
<FileFormat> ::= csv|html|txt|tsv|jpeg|jpg|png|svg|pdf|rtf|pptx|xlsx|docx|odt|ods|openoffice|ms|microsoft|micro$oft
<Language> ::= ar|bn|bg|ca|zh-CN|zh-TW|hr|cs|da|nl|en|en-GB|et|fi|fr|de|el|gu|iw|is|in|it|ja|kn|ko|lv|lt|ms|ml|mr|no|or|fa|pl|pt-BR|pt-PT|ro|ru|sr|sk|sl|es|sv|tl|ta|te|th|tr|uk|ur|vi
# Basic items built from primatives
Basic items built from primitives
<Boolean> ::= <TrueValues>|<FalseValues>
<ByteCount> ::= <Number>[m|k|b]
<CIDRnetmask> ::= <Number>.<Number>.<Number>.<Number>/<Number>
@@ -46,7 +53,7 @@ If an item contains spaces, it should be surrounded by " or '.
<Tag> ::= <String>
<UniqueID> ::= uid:<String>
# Named items
Named items
<AccessToken> ::= <String>
<ACLScope> ::= [user:]<EmailAddress>|group:<EmailAddress>|domain[:<DomainName>]|default
<CalendarACLRole> ::= editor|freebusy|freebusyreader|owner|reader|writer
@@ -297,12 +304,12 @@ If an item contains spaces, it should be surrounded by " or '.
<UserOrderByFieldName> ::=
familyname|lastname|givenname|firstname|email
# Named Lists
# Lists can be in the following formats
# Items, separated by commas, without spaces or commas in the items themselves: item(,item)*
# Items, separated by spaces, without spaces or commas in the items themselves: "item( item)*"
# Items, separated by commas, with spaces or commas in the items themselves: "'it em'(,'it em')*"
# Items, separated by spaces, with spaces or commas in the items themselves: "'it em'( 'it em')*"
Named Lists
Lists can be in the following formats
Items, separated by commas, without spaces or commas in the items themselves: item(,item)*
Items, separated by spaces, without spaces or commas in the items themselves: "item( item)*"
Items, separated by commas, with spaces or commas in the items themselves: "'it em'(,'it em')*"
Items, separated by spaces, with spaces or commas in the items themselves: "'it em'( 'it em')*"
<ACLList> ::== '<ACLScope>(,<ACLScope>)*'
<CalendarList> ::= '<CalendarItem>(,<CalendarItem>)*'
@@ -331,11 +338,11 @@ If an item contains spaces, it should be surrounded by " or '.
<UserFieldNameList> ::= '<UserFieldName>(,<UserFieldName>)*'
<UserList> ::= '<UserItem>(,<UserItem>)*'
# Specify a collection of ChromeOS devices by directly specifying them
Specify a collection of ChromeOS devices by directly specifying them
<CrOSTypeEntity> ::=
(all cros)|
(cros <CrOSList>)|
# Specify a collection of Users by directly specifying them or by specifiying items that will yield a list of users
Specify a collection of Users by directly specifying them or by specifiying items that will yield a list of users
<UserTypeEntity> ::=
(all users)|
(user <UserItem>)|
@@ -352,7 +359,7 @@ If an item contains spaces, it should be surrounded by " or '.
(query <QueryUser>)
# Item attributes
Item attributes
<CalendarAttributes> ::=
(selected <Boolean>)|(hidden <Boolean>)|(summary <String>)|(colorindex|colorid <CalendarColorIndex>)|(backgroundcolor <ColorHex>)|(foregroundcolor <ColorHex>)|
(reminder clear|(email|sms|pop <Number>))|
@@ -470,18 +477,21 @@ gam help
gam batch <FileName>|- [charset <Charset>]
gam csv <FileName>|- [charset <Charset>] gam <GAM argument list>
# You can make substitutions in <GAMArgumentList> with values from the CSV file.
# An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file
# An argument containing instances of ~~xxx~~ has xxx replaced by the value of field xxx from the CSV file
You can make substitutions in <GAMArgumentList> with values from the CSV file.
An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file
An argument containing instances of ~~xxx~~ has xxx replaced by the value of field xxx from the CSV file
# 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
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 oauth|oauth2 create|request [<EmailAddress>]
gam oauth|oauth2 delete|revoke
gam oauth|oauth2 info|verify [<AccessToken>]
gam <UserTypeEntity> check serviceaccount
gam whatis <EmailItem>
gam report users|user [todrive]
@@ -582,8 +592,8 @@ gam info group <GroupItem> [nousers] [noaliases] [groups]
gam update group <GroupItem> clear [member] [manager] [owner]
gam print groups [todrive] ([domain <DomainName>] [member <UserItem>])
[maxresults <Number>] [delimiter <String>]
[members] [managers] [owners] [settings] <GroupFieldName>* [fields <GroupFieldNameList>]
[maxresults <Number>] [allfields|([settings] <GroupFieldName>* [fields <GroupFieldNameList>])] [delimiter <String>]
[members|memberscount] [managers|managerscount] [owners|ownerscount]
gam print group-members|groups-members [todrive] ([domain <DomainName>] [member <UserItem>])|[group <GroupItem>]
[membernames] [fields <MembersFieldNameList>]
@@ -633,7 +643,7 @@ gam create course id|alias <CourseAlias> [teacher <UserItem>] <CourseAttributes>
gam update course <CourseID> <CourseAttributes>+
gam delete course <CourseID>
gam info course <CourseID>
gam print courses [todrive] [teacher] [student] [alias|aliases] [delimiter <String>]
gam print courses [todrive] [teacher <UserItem>] [student <UserItem>] [alias|aliases] [delimiter <String>]
gam course <CourseID> add alias <CourseAlias>
gam course <CourseID> delete alias <CourseAlias>
@@ -711,7 +721,7 @@ gam <UserTypeEntity> delete|del emptydrivefolders
gam <UserTypeEntity> empty drivetrash
gam <UserTypeEntity> add drivefileacl <DriveFileID> anyone|(user <UserItem>)|(group <GroupItem>)|(domain <DomainName>)
(role <DriveFileACLRole>) [withlink] [sendmail] [emailmessage <String>]
(role <DriveFileACLRole>) [withlink] [sendemail] [emailmessage <String>]
gam <UserTypeEntity> update drivefileacl <DriveFileID> <PermissionID>
(role <DriveFileACLRole>) [withlink] [transferownership <Boolean>]
gam <UserTypeEntity> delete|del drivefileacl <DriveFileID> <PermissionID>
@@ -740,9 +750,7 @@ gam print tokens|token [todrive] [clientid <ClientID>] [<UserTypeEntity>]
gam <UserTypeEntity> update user <UserAttrubutes>
gam <UserTypeEntity> deprovision|deprov
#
# Update user Gmail mailbox
#
gam <UserTypeEntity> [add] label|labels <Name> [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
gam <UserTypeEntity> update labelsettings <LabelName> [name <Name>] [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
gam <UserTypeEntity> update label|labels [search <RegularExpression>] [replace <LabelReplacement>] [merge]
@@ -753,9 +761,7 @@ gam <UserTypeEntity> delete messages query <QueryGmail> [doit] [max_to_delete|ma
gam <UserTypeEntity> modify messages query <QueryGmail> [doit] [max_to_modify|max_to_process <Number>] (addlabel <LabelName>)* (removelabel <LabelName>)*
gam <UserTypeEntity> trash messages query <QueryGmail> [doit] [max_to_trash|max_to_process <Number>]
gam <UserTypeEntity> untrash messages query <QueryGmail> [doit] [max_to_untrash|max_to_process <Number>]
#
# Update user Gmail settings
#
gam <UserTypeEntity> show gmailprofile [todrive]
gam <UserTypeEntity> show gplusprofile [todrive]
@@ -789,14 +795,14 @@ 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> [add] sendas <EmailAddress> <Name> [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)*] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
gam <UserTypeEntity> update sendas <EmailAddress> [name <Name>] [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)*] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
gam <UserTypeEntity> [add] sendas <EmailAddress> <Name> [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)*] [html] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
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]
gam <UserTypeEntity> info sendas <EmailAddress> [format]
gam <UserTypeEntity> print sendas [todrive]
gam <UserTypeEntity> signature|sig <String>|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)* [name <String>] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
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]
gam <UserTypeEntity> vacation <FalseValues>

1544
src/gam.py

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
__version__ = "1.5.5"
__version__ = "1.6.1"
# Set default logging handler to avoid "No handler found" warnings.
import logging

View File

@@ -0,0 +1,91 @@
# 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
import google_auth_httplib2
HAS_GOOGLE_AUTH = True
except ImportError: # pragma: NO COVER
HAS_GOOGLE_AUTH = False
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.
"""
if HAS_GOOGLE_AUTH and isinstance(
credentials, google.auth.credentials.Credentials):
return google_auth_httplib2.AuthorizedHttp(credentials)
else:
return credentials.authorize(httplib2.Http())

View File

@@ -53,6 +53,7 @@ import httplib2
import uritemplate
# Local imports
from googleapiclient import _auth
from googleapiclient import mimeparse
from googleapiclient.errors import HttpError
from googleapiclient.errors import InvalidJsonError
@@ -197,7 +198,8 @@ def build(serviceName,
model: googleapiclient.Model, converts to and from the wire format.
requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
request.
credentials: oauth2client.Credentials, credentials to be used for
credentials: oauth2client.Credentials or
google.auth.credentials.Credentials, credentials to be used for
authentication.
cache_discovery: Boolean, whether or not to cache the discovery doc.
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
@@ -211,15 +213,14 @@ def build(serviceName,
'apiVersion': version
}
if http is None:
http = httplib2.Http()
discovery_http = http if http is not None else httplib2.Http()
for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI,):
requested_url = uritemplate.expand(discovery_url, params)
try:
content = _retrieve_discovery_doc(requested_url, http, cache_discovery,
cache)
content = _retrieve_discovery_doc(
requested_url, discovery_http, cache_discovery, cache)
return build_from_document(content, base=discovery_url, http=http,
developerKey=developerKey, model=model, requestBuilder=requestBuilder,
credentials=credentials)
@@ -316,17 +317,16 @@ def build_from_document(
model: Model class instance that serializes and de-serializes requests and
responses.
requestBuilder: Takes an http request and packages it up to be executed.
credentials: object, credentials to be used for authentication.
credentials: oauth2client.Credentials or
google.auth.credentials.Credentials, credentials to be used for
authentication.
Returns:
A Resource object with methods for interacting with the service.
"""
if http is None:
http = httplib2.Http()
# future is no longer used.
future = {}
if http is not None and credentials is not None:
raise ValueError('Arguments http and credentials are mutually exclusive.')
if isinstance(service, six.string_types):
service = json.loads(service)
@@ -342,31 +342,36 @@ def build_from_document(
base = urljoin(service['rootUrl'], service['servicePath'])
schema = Schemas(service)
if credentials:
# If credentials were passed in, we could have two cases:
# 1. the scopes were specified, in which case the given credentials
# are used for authorizing the http;
# 2. the scopes were not provided (meaning the Application Default
# Credentials are to be used). In this case, the Application Default
# Credentials are built and used instead of the original credentials.
# If there are no scopes found (meaning the given service requires no
# authentication), there is no authorization of the http.
if (isinstance(credentials, GoogleCredentials) and
credentials.create_scoped_required()):
scopes = service.get('auth', {}).get('oauth2', {}).get('scopes', {})
if scopes:
credentials = credentials.create_scoped(list(scopes.keys()))
else:
# No need to authorize the http object
# if the service does not require authentication.
credentials = None
# If the http client is not specified, then we must construct an http client
# to make requests. If the service has scopes, then we also need to setup
# authentication.
if http is None:
# Does the service require scopes?
scopes = list(
service.get('auth', {}).get('oauth2', {}).get('scopes', {}).keys())
if credentials:
http = credentials.authorize(http)
# If so, then the we need to setup authentication.
if scopes:
# If the user didn't pass in credentials, attempt to acquire application
# default credentials.
if credentials is None:
credentials = _auth.default_credentials()
# The credentials need to be scoped.
credentials = _auth.with_scopes(credentials, scopes)
# Create an authorized http instance
http = _auth.authorized_http(credentials)
# If the service doesn't require scopes then there is no need for
# authentication.
else:
http = httplib2.Http()
if model is None:
features = service.get('features', [])
model = JsonModel('dataWrapper' in features)
return Resource(http=http, baseUrl=base, model=model,
developerKey=developerKey, requestBuilder=requestBuilder,
resourceDesc=service, rootDesc=service, schema=schema)

View File

@@ -996,7 +996,11 @@ class HttpRequest(object):
elif resp.status == 308:
self._in_error_state = False
# A "308 Resume Incomplete" indicates we are not done.
self.resumable_progress = int(resp['range'].split('-')[1]) + 1
try:
self.resumable_progress = int(resp['range'].split('-')[1]) + 1
except KeyError:
# If resp doesn't contain range header, resumable progress is 0
self.resumable_progress = 0
if 'location' in resp:
self.resumable_uri = resp['location']
else:

View File

@@ -1,11 +1,12 @@
rm -rf gam
rm -rf build
rm -rf dist
rm -rf dist
rm -rf gam-$1-linux-$(arch).tar.xz
export LD_LIBRARY_PATH=/usr/local/lib
pyinstaller --clean -F --distpath=gam linux-gam.spec
cp LICENSE gam
cp whatsnew.txt gam
cp GamCommands.txt gam
tar cfJ gam-$1-linux-$(arch).tar.xz gam/

View File

@@ -1,10 +1,11 @@
rmdir /q /s gam
rmdir /q /s build
rmdir /q /s dist
rm -rf gam
rm -rf build
rm -rf dist
rm -rf gam-$1-macos.tar.xz
/Library/Frameworks/Python.framework/Versions/2.7/bin/pyinstaller --clean -F --distpath=gam macos-gam.spec
cp LICENSE gam
cp whatsnew.txt gam
cp GamCommands.txt gam
tar cfJ gam-$1-macos.tar.xz gam/

View File

@@ -14,7 +14,7 @@
"""Client library for using OAuth2, especially with Google APIs."""
__version__ = '3.0.0'
__version__ = '4.0.0'
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'

View File

@@ -11,12 +11,248 @@
# 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):

65
src/oauth2client/_pkce.py Normal file
View File

@@ -0,0 +1,65 @@
# 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))
# 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.
"""
return base64.urlsafe_b64encode(hashlib.sha256(verifier).digest())

View File

@@ -34,13 +34,11 @@ from six.moves import urllib
import oauth2client
from oauth2client import _helpers
from oauth2client import _pkce
from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client import util
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
HAS_OPENSSL = False
HAS_CRYPTO = False
try:
@@ -100,20 +98,20 @@ AccessTokenInfo = collections.namedtuple(
DEFAULT_ENV_NAME = 'UNKNOWN'
# If set to True _get_environment avoid GCE check (_detect_gce_environment)
NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False')
NO_GCE_CHECK = os.getenv('NO_GCE_CHECK', 'False')
# Timeout in seconds to wait for the GCE metadata server when detecting the
# GCE environment.
try:
GCE_METADATA_TIMEOUT = int(
os.environ.setdefault('GCE_METADATA_TIMEOUT', '3'))
GCE_METADATA_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3))
except ValueError: # pragma: NO COVER
GCE_METADATA_TIMEOUT = 3
_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
_GCE_METADATA_HOST = '169.254.169.254'
_METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
_GCE_METADATA_URI = 'http://169.254.169.254'
_METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header
_DESIRED_METADATA_FLAVOR = 'Google'
_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
# Expose utcnow() at module level to allow for
# easier testing (by replacing with a stub).
@@ -440,23 +438,6 @@ class Storage(object):
self.release_lock()
def _update_query_params(uri, params):
"""Updates a URI with new query parameters.
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 = dict(urllib.parse.parse_qsl(parts.query))
query_params.update(params)
new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
return urllib.parse.urlunparse(new_parts)
class OAuth2Credentials(Credentials):
"""Credentials object for OAuth 2.0.
@@ -466,7 +447,7 @@ class OAuth2Credentials(Credentials):
OAuth2Credentials objects may be safely pickled and unpickled.
"""
@util.positional(8)
@_helpers.positional(8)
def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, revoke_uri=None,
id_token=None, token_response=None, scopes=None,
@@ -513,7 +494,7 @@ class OAuth2Credentials(Credentials):
self.revoke_uri = revoke_uri
self.id_token = id_token
self.token_response = token_response
self.scopes = set(util.string_to_scopes(scopes or []))
self.scopes = set(_helpers.string_to_scopes(scopes or []))
self.token_info_uri = token_info_uri
# True if the credentials have been revoked or expired and can't be
@@ -557,7 +538,7 @@ class OAuth2Credentials(Credentials):
http: httplib2.Http, an http object to be used to make the refresh
request.
"""
self._refresh(http.request)
self._refresh(http)
def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void.
@@ -566,7 +547,7 @@ class OAuth2Credentials(Credentials):
http: httplib2.Http, an http object to be used to make the revoke
request.
"""
self._revoke(http.request)
self._revoke(http)
def apply(self, headers):
"""Add the authorization to the headers.
@@ -592,7 +573,7 @@ class OAuth2Credentials(Credentials):
not have scopes. In both cases, you can use refresh_scopes() to
obtain the canonical set of scopes.
"""
scopes = util.string_to_scopes(scopes)
scopes = _helpers.string_to_scopes(scopes)
return set(scopes).issubset(self.scopes)
def retrieve_scopes(self, http):
@@ -607,7 +588,7 @@ class OAuth2Credentials(Credentials):
Returns:
A set of strings containing the canonical list of scopes.
"""
self._retrieve_scopes(http.request)
self._retrieve_scopes(http)
return self.scopes
@classmethod
@@ -746,7 +727,7 @@ class OAuth2Credentials(Credentials):
return headers
def _refresh(self, http_request):
def _refresh(self, http):
"""Refreshes the access_token.
This method first checks by reading the Storage object if available.
@@ -754,15 +735,13 @@ class OAuth2Credentials(Credentials):
refresh is completed.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
refresh request.
http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
"""
if not self.store:
self._do_refresh_request(http_request)
self._do_refresh_request(http)
else:
self.store.acquire_lock()
try:
@@ -774,17 +753,15 @@ class OAuth2Credentials(Credentials):
logger.info('Updated access_token read from Storage')
self._updateFromCredential(new_cred)
else:
self._do_refresh_request(http_request)
self._do_refresh_request(http)
finally:
self.store.release_lock()
def _do_refresh_request(self, http_request):
def _do_refresh_request(self, http):
"""Refresh the access_token using the refresh_token.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
refresh request.
http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
@@ -793,8 +770,9 @@ class OAuth2Credentials(Credentials):
headers = self._generate_refresh_request_headers()
logger.info('Refreshing access_token')
resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers)
resp, content = transport.request(
http, self.token_uri, method='POST',
body=body, headers=headers)
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
d = json.loads(content)
@@ -819,7 +797,7 @@ class OAuth2Credentials(Credentials):
# An {'error':...} response body means the token is expired or
# revoked, so we flag the credentials as such.
logger.info('Failed to retrieve access token: %s', content)
error_msg = 'Invalid response {0}.'.format(resp['status'])
error_msg = 'Invalid response {0}.'.format(resp.status)
try:
d = json.loads(content)
if 'error' in d:
@@ -833,23 +811,19 @@ class OAuth2Credentials(Credentials):
pass
raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
def _revoke(self, http_request):
def _revoke(self, http):
"""Revokes this credential and deletes the stored copy (if it exists).
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
revoke request.
http: an object to be used to make HTTP requests.
"""
self._do_revoke(http_request, self.refresh_token or self.access_token)
self._do_revoke(http, self.refresh_token or self.access_token)
def _do_revoke(self, http_request, token):
def _do_revoke(self, http, token):
"""Revokes this credential and deletes the stored copy (if it exists).
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
refresh request.
http: an object to be used to make HTTP requests.
token: A string used as the token to be revoked. Can be either an
access_token or refresh_token.
@@ -859,8 +833,13 @@ class OAuth2Credentials(Credentials):
"""
logger.info('Revoking token')
query_params = {'token': token}
token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
resp, content = http_request(token_revoke_uri)
token_revoke_uri = _helpers.update_query_params(
self.revoke_uri, query_params)
resp, content = transport.request(http, token_revoke_uri)
if resp.status == http_client.METHOD_NOT_ALLOWED:
body = urllib.parse.urlencode(query_params)
resp, content = transport.request(http, token_revoke_uri,
method='POST', body=body)
if resp.status == http_client.OK:
self.invalid = True
else:
@@ -876,23 +855,19 @@ class OAuth2Credentials(Credentials):
if self.store:
self.store.delete()
def _retrieve_scopes(self, http_request):
def _retrieve_scopes(self, http):
"""Retrieves the list of authorized scopes from the OAuth2 provider.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
revoke request.
http: an object to be used to make HTTP requests.
"""
self._do_retrieve_scopes(http_request, self.access_token)
self._do_retrieve_scopes(http, self.access_token)
def _do_retrieve_scopes(self, http_request, token):
def _do_retrieve_scopes(self, http, token):
"""Retrieves the list of authorized scopes from the OAuth2 provider.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
refresh request.
http: an object to be used to make HTTP requests.
token: A string used as the token to identify the credentials to
the provider.
@@ -902,13 +877,13 @@ class OAuth2Credentials(Credentials):
"""
logger.info('Refreshing scopes')
query_params = {'access_token': token, 'fields': 'scope'}
token_info_uri = _update_query_params(self.token_info_uri,
query_params)
resp, content = http_request(token_info_uri)
token_info_uri = _helpers.update_query_params(
self.token_info_uri, query_params)
resp, content = transport.request(http, token_info_uri)
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
d = json.loads(content)
self.scopes = set(util.string_to_scopes(d.get('scope', '')))
self.scopes = set(_helpers.string_to_scopes(d.get('scope', '')))
else:
error_msg = 'Invalid response {0}.'.format(resp.status)
try:
@@ -977,19 +952,25 @@ class AccessTokenCredentials(OAuth2Credentials):
data['user_agent'])
return retval
def _refresh(self, http_request):
def _refresh(self, http):
"""Refreshes the access token.
Args:
http: unused HTTP object.
Raises:
AccessTokenCredentialsError: always
"""
raise AccessTokenCredentialsError(
'The access_token is expired or invalid and can\'t be refreshed.')
def _revoke(self, http_request):
def _revoke(self, http):
"""Revokes the access_token and deletes the store if available.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
revoke request.
http: an object to be used to make HTTP requests.
"""
self._do_revoke(http_request, self.access_token)
self._do_revoke(http, self.access_token)
def _detect_gce_environment():
@@ -1005,21 +986,16 @@ def _detect_gce_environment():
# 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".
connection = six.moves.http_client.HTTPConnection(
_GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT)
http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT)
try:
headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
connection.request('GET', '/', headers=headers)
response = connection.getresponse()
if response.status == http_client.OK:
return (response.getheader(_METADATA_FLAVOR_HEADER) ==
_DESIRED_METADATA_FLAVOR)
response, _ = transport.request(
http, _GCE_METADATA_URI, headers=_GCE_HEADERS)
return (
response.status == http_client.OK and
response.get(_METADATA_FLAVOR_HEADER) == _DESIRED_METADATA_FLAVOR)
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
logger.info('Timeout attempting to reach GCE metadata service.')
return False
finally:
connection.close()
def _in_gae_environment():
@@ -1469,7 +1445,7 @@ class AssertionCredentials(GoogleCredentials):
AssertionCredentials objects may be safely pickled and unpickled.
"""
@util.positional(2)
@_helpers.positional(2)
def __init__(self, assertion_type, user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
@@ -1511,15 +1487,13 @@ class AssertionCredentials(GoogleCredentials):
"""Generate assertion string to be used in the access token request."""
raise NotImplementedError
def _revoke(self, http_request):
def _revoke(self, http):
"""Revokes the access_token and deletes the store if available.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
revoke request.
http: an object to be used to make HTTP requests.
"""
self._do_revoke(http_request, self.access_token)
self._do_revoke(http, self.access_token)
def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
@@ -1545,7 +1519,7 @@ def _require_crypto_or_die():
raise CryptoUnavailableError('No crypto library available')
@util.positional(2)
@_helpers.positional(2)
def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATION_CERTS):
"""Verifies a signed JWT id_token.
@@ -1572,7 +1546,7 @@ def verify_id_token(id_token, audience, http=None,
if http is None:
http = transport.get_cached_http()
resp, content = http.request(cert_uri)
resp, content = transport.request(http, cert_uri)
if resp.status == http_client.OK:
certs = json.loads(_helpers._from_bytes(content))
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
@@ -1624,7 +1598,7 @@ def _parse_exchange_token_response(content):
except Exception:
# different JSON libs raise different exceptions,
# so we just do a catch-all here
resp = dict(urllib.parse.parse_qsl(content))
resp = _helpers.parse_unique_urlencoded(content)
# some providers respond with 'expires', others with 'expires_in'
if resp and 'expires' in resp:
@@ -1633,7 +1607,7 @@ def _parse_exchange_token_response(content):
return resp
@util.positional(4)
@_helpers.positional(4)
def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None,
user_agent=None,
@@ -1641,7 +1615,9 @@ def credentials_from_code(client_id, client_secret, scope, code,
auth_uri=oauth2client.GOOGLE_AUTH_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI):
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
pkce=False,
code_verifier=None):
"""Exchanges an authorization code for an OAuth2Credentials object.
Args:
@@ -1665,6 +1641,15 @@ def credentials_from_code(client_id, client_secret, scope, code,
device_uri: string, URI for device authorization endpoint. For
convenience defaults to Google's endpoints but any OAuth
2.0 provider can be used.
pkce: boolean, default: False, Generate and include a "Proof Key
for Code Exchange" (PKCE) with your authorization and token
requests. This adds security for installed applications that
cannot protect a client_secret. See RFC 7636 for details.
code_verifier: bytestring or None, default: None, parameter passed
as part of the code exchange when pkce=True. If
None, a code_verifier will automatically be
generated as part of step1_get_authorize_url(). See
RFC 7636 for details.
Returns:
An OAuth2Credentials object.
@@ -1675,16 +1660,20 @@ def credentials_from_code(client_id, client_secret, scope, code,
"""
flow = OAuth2WebServerFlow(client_id, client_secret, scope,
redirect_uri=redirect_uri,
user_agent=user_agent, auth_uri=auth_uri,
token_uri=token_uri, revoke_uri=revoke_uri,
user_agent=user_agent,
auth_uri=auth_uri,
token_uri=token_uri,
revoke_uri=revoke_uri,
device_uri=device_uri,
token_info_uri=token_info_uri)
token_info_uri=token_info_uri,
pkce=pkce,
code_verifier=code_verifier)
credentials = flow.step2_exchange(code, http=http)
return credentials
@util.positional(3)
@_helpers.positional(3)
def credentials_from_clientsecrets_and_code(filename, scope, code,
message=None,
redirect_uri='postmessage',
@@ -1713,6 +1702,15 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
device_uri: string, OAuth 2.0 device authorization endpoint
pkce: boolean, default: False, Generate and include a "Proof Key
for Code Exchange" (PKCE) with your authorization and token
requests. This adds security for installed applications that
cannot protect a client_secret. See RFC 7636 for details.
code_verifier: bytestring or None, default: None, parameter passed
as part of the code exchange when pkce=True. If
None, a code_verifier will automatically be
generated as part of step1_get_authorize_url(). See
RFC 7636 for details.
Returns:
An OAuth2Credentials object.
@@ -1803,7 +1801,7 @@ class OAuth2WebServerFlow(Flow):
OAuth2WebServerFlow objects may be safely pickled and unpickled.
"""
@util.positional(4)
@_helpers.positional(4)
def __init__(self, client_id,
client_secret=None,
scope=None,
@@ -1816,6 +1814,8 @@ class OAuth2WebServerFlow(Flow):
device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
authorization_header=None,
pkce=False,
code_verifier=None,
**kwargs):
"""Constructor for OAuth2WebServerFlow.
@@ -1853,6 +1853,15 @@ class OAuth2WebServerFlow(Flow):
require a client to authenticate using a
header value instead of passing client_secret
in the POST body.
pkce: boolean, default: False, Generate and include a "Proof Key
for Code Exchange" (PKCE) with your authorization and token
requests. This adds security for installed applications that
cannot protect a client_secret. See RFC 7636 for details.
code_verifier: bytestring or None, default: None, parameter passed
as part of the code exchange when pkce=True. If
None, a code_verifier will automatically be
generated as part of step1_get_authorize_url(). See
RFC 7636 for details.
**kwargs: dict, The keyword arguments are all optional and required
parameters for the OAuth calls.
"""
@@ -1862,7 +1871,7 @@ class OAuth2WebServerFlow(Flow):
raise TypeError("The value of scope must not be None")
self.client_id = client_id
self.client_secret = client_secret
self.scope = util.scopes_to_string(scope)
self.scope = _helpers.scopes_to_string(scope)
self.redirect_uri = redirect_uri
self.login_hint = login_hint
self.user_agent = user_agent
@@ -1872,9 +1881,11 @@ class OAuth2WebServerFlow(Flow):
self.device_uri = device_uri
self.token_info_uri = token_info_uri
self.authorization_header = authorization_header
self._pkce = pkce
self.code_verifier = code_verifier
self.params = _oauth2_web_server_flow_params(kwargs)
@util.positional(1)
@_helpers.positional(1)
def step1_get_authorize_url(self, redirect_uri=None, state=None):
"""Returns a URI to redirect to the provider.
@@ -1912,10 +1923,17 @@ class OAuth2WebServerFlow(Flow):
query_params['state'] = state
if self.login_hint is not None:
query_params['login_hint'] = self.login_hint
query_params.update(self.params)
return _update_query_params(self.auth_uri, query_params)
if self._pkce:
if not self.code_verifier:
self.code_verifier = _pkce.code_verifier()
challenge = _pkce.code_challenge(self.code_verifier)
query_params['code_challenge'] = challenge
query_params['code_challenge_method'] = 'S256'
@util.positional(1)
query_params.update(self.params)
return _helpers.update_query_params(self.auth_uri, query_params)
@_helpers.positional(1)
def step1_get_device_and_user_codes(self, http=None):
"""Returns a user code and the verification URL where to enter it
@@ -1940,8 +1958,8 @@ class OAuth2WebServerFlow(Flow):
if http is None:
http = transport.get_http_object()
resp, content = http.request(self.device_uri, method='POST', body=body,
headers=headers)
resp, content = transport.request(
http, self.device_uri, method='POST', body=body, headers=headers)
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
try:
@@ -1963,7 +1981,7 @@ class OAuth2WebServerFlow(Flow):
pass
raise OAuth2DeviceCodeError(error_msg)
@util.positional(2)
@_helpers.positional(2)
def step2_exchange(self, code=None, http=None, device_flow_info=None):
"""Exchanges a code for OAuth2Credentials.
@@ -2006,6 +2024,8 @@ class OAuth2WebServerFlow(Flow):
}
if self.client_secret is not None:
post_data['client_secret'] = self.client_secret
if self._pkce:
post_data['code_verifier'] = self.code_verifier
if device_flow_info is not None:
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
else:
@@ -2023,8 +2043,8 @@ class OAuth2WebServerFlow(Flow):
if http is None:
http = transport.get_http_object()
resp, content = http.request(self.token_uri, method='POST', body=body,
headers=headers)
resp, content = transport.request(
http, self.token_uri, method='POST', body=body, headers=headers)
d = _parse_exchange_token_response(content)
if resp.status == http_client.OK and 'access_token' in d:
access_token = d['access_token']
@@ -2060,10 +2080,10 @@ class OAuth2WebServerFlow(Flow):
raise FlowExchangeError(error_msg)
@util.positional(2)
@_helpers.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None, login_hint=None,
device_uri=None):
device_uri=None, pkce=None, code_verifier=None):
"""Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the
@@ -2112,10 +2132,11 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
'login_hint': login_hint,
}
revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None:
constructor_kwargs['revoke_uri'] = revoke_uri
if device_uri is not None:
constructor_kwargs['device_uri'] = device_uri
optional = ('revoke_uri', 'device_uri', 'pkce', 'code_verifier')
for param in optional:
if locals()[param] is not None:
constructor_kwargs[param] = locals()[param]
return OAuth2WebServerFlow(
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)

View File

@@ -22,7 +22,6 @@ import json
import six
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
# Properties that make a client_secrets.json file valid.
TYPE_WEB = 'web'

View File

@@ -20,28 +20,25 @@ See https://cloud.google.com/compute/docs/metadata
import datetime
import json
import httplib2
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 util
from oauth2client import transport
METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
def get(http_request, path, root=METADATA_ROOT, recursive=None):
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/defualt'
http_request: A callable that matches the method
signature of httplib2.Http.request. Used to make the request to the
metadataserver.
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
@@ -51,15 +48,14 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
A dictionary if the metadata server returns JSON, otherwise a string.
Raises:
httplib2.Httplib2Error if an error corrured while retrieving metadata.
http_client.HTTPException if an error corrured while
retrieving metadata.
"""
url = urlparse.urljoin(root, path)
url = util._add_query_parameter(url, 'recursive', recursive)
url = _helpers._add_query_parameter(url, 'recursive', recursive)
response, content = http_request(
url,
headers=METADATA_HEADERS
)
response, content = transport.request(
http, url, headers=METADATA_HEADERS)
if response.status == http_client.OK:
decoded = _helpers._from_bytes(content)
@@ -68,21 +64,20 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
else:
return decoded
else:
raise httplib2.HttpLib2Error(
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_request, service_account='default'):
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.
http_request: A callable that matches the method
signature of httplib2.Http.request. Used to make the request to the
metadata server.
Returns:
A dictionary with information about the specified service account,
for example:
@@ -94,21 +89,19 @@ def get_service_account_info(http_request, service_account='default'):
}
"""
return get(
http_request,
http,
'instance/service-accounts/{0}/'.format(service_account),
recursive=True)
def get_token(http_request, service_account='default'):
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.
http_request: A callable that matches the method
signature of httplib2.Http.request. Used to make the request to the
metadataserver.
Returns:
A tuple of (access token, token expiration), where access token is the
@@ -116,7 +109,7 @@ def get_token(http_request, service_account='default'):
that indicates when the access token will expire.
"""
token_json = get(
http_request,
http,
'instance/service-accounts/{0}/token'.format(service_account))
token_expiry = client._UTCNOW() + datetime.timedelta(
seconds=token_json['expires_in'])

View File

@@ -29,13 +29,13 @@ 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 httplib2
import webapp2 as webapp
import oauth2client
from oauth2client import _helpers
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client import util
from oauth2client import transport
from oauth2client.contrib import xsrfutil
# This is a temporary fix for a Google internal issue.
@@ -45,8 +45,6 @@ except ImportError: # pragma: NO COVER
_appengine_ndb = None
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
logger = logging.getLogger(__name__)
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
@@ -131,7 +129,7 @@ class AppAssertionCredentials(client.AssertionCredentials):
information to generate and refresh its own access tokens.
"""
@util.positional(2)
@_helpers.positional(2)
def __init__(self, scope, **kwargs):
"""Constructor for AppAssertionCredentials
@@ -143,7 +141,7 @@ class AppAssertionCredentials(client.AssertionCredentials):
or unspecified, the default service account for
the app is used.
"""
self.scope = util.scopes_to_string(scope)
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
@@ -157,17 +155,15 @@ class AppAssertionCredentials(client.AssertionCredentials):
data = json.loads(json_data)
return AppAssertionCredentials(data['scope'])
def _refresh(self, http_request):
"""Refreshes the access_token.
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_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
refresh request.
http: unused HTTP object
Raises:
AccessTokenRefreshError: When the refresh fails.
@@ -305,7 +301,7 @@ class StorageByKeyName(client.Storage):
and that entities are stored by key_name.
"""
@util.positional(4)
@_helpers.positional(4)
def __init__(self, model, key_name, property_name, cache=None, user=None):
"""Constructor for Storage.
@@ -523,7 +519,7 @@ class OAuth2Decorator(object):
flow = property(get_flow, set_flow)
@util.positional(4)
@_helpers.positional(4)
def __init__(self, client_id, client_secret, scope,
auth_uri=oauth2client.GOOGLE_AUTH_URI,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
@@ -590,7 +586,7 @@ class OAuth2Decorator(object):
self.credentials = None
self._client_id = client_id
self._client_secret = client_secret
self._scope = util.scopes_to_string(scope)
self._scope = _helpers.scopes_to_string(scope)
self._auth_uri = auth_uri
self._token_uri = token_uri
self._revoke_uri = revoke_uri
@@ -742,7 +738,8 @@ class OAuth2Decorator(object):
*args: Positional arguments passed to httplib2.Http constructor.
**kwargs: Positional arguments passed to httplib2.Http constructor.
"""
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
return self.credentials.authorize(
transport.get_http_object(*args, **kwargs))
@property
def callback_path(self):
@@ -804,7 +801,7 @@ class OAuth2Decorator(object):
if (decorator._token_response_param and
credentials.token_response):
resp_json = json.dumps(credentials.token_response)
redirect_uri = util._add_query_parameter(
redirect_uri = _helpers._add_query_parameter(
redirect_uri, decorator._token_response_param,
resp_json)
@@ -848,7 +845,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
"""
@util.positional(3)
@_helpers.positional(3)
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
"""Constructor
@@ -891,7 +888,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
self._message = 'Please configure your application for OAuth 2.0.'
@util.positional(2)
@_helpers.positional(2)
def oauth2decorator_from_clientsecrets(filename, scope,
message=None, cache=None):
"""Creates an OAuth2Decorator populated from a clientsecrets file.

View File

@@ -117,7 +117,12 @@ class DevshellCredentials(client.GoogleCredentials):
user_agent)
self._refresh(None)
def _refresh(self, http_request):
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

View File

@@ -52,6 +52,9 @@ Add the helper to your INSTALLED_APPS:
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
@@ -228,10 +231,10 @@ import importlib
import django.conf
from django.core import exceptions
from django.core import urlresolvers
import httplib2
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
@@ -335,16 +338,26 @@ class OAuth2Settings(object):
self.request_prefix = getattr(settings_instance,
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
self.client_id, self.client_secret = \
_get_oauth2_client_id_and_secret(settings_instance)
info = _get_oauth2_client_id_and_secret(settings_instance)
self.client_id, self.client_secret = info
if ('django.contrib.sessions.middleware.SessionMiddleware'
not in settings_instance.MIDDLEWARE_CLASSES):
# 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(
'The Google OAuth2 Helper requires session middleware to '
'be installed. Edit your MIDDLEWARE_CLASSES setting'
' to include \'django.contrib.sessions.middleware.'
'SessionMiddleware\'.')
'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()
@@ -470,8 +483,7 @@ class UserOAuth2(object):
@property
def http(self):
"""Helper method to create an HTTP client authorized with OAuth2
credentials."""
"""Helper: create HTTP client authorized with OAuth2 credentials."""
if self.has_credentials():
return self.credentials.authorize(httplib2.Http())
return self.credentials.authorize(transport.get_http_object())
return None

View File

@@ -22,13 +22,13 @@ in the configured storage."""
import hashlib
import json
import os
import pickle
from django import http
from django import shortcuts
from django.conf import settings
from django.core import urlresolvers
from django.shortcuts import redirect
import jsonpickle
from six.moves.urllib import parse
from oauth2client import client
@@ -71,7 +71,7 @@ def _make_flow(request, scopes, return_url=None):
urlresolvers.reverse("google_oauth:callback")))
flow_key = _FLOW_KEY.format(csrf_token)
request.session[flow_key] = pickle.dumps(flow)
request.session[flow_key] = jsonpickle.encode(flow)
return flow
@@ -89,7 +89,7 @@ def _get_flow_for_token(csrf_token, request):
CSRF token.
"""
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
return None if flow_pickle is None else pickle.loads(flow_pickle)
return None if flow_pickle is None else jsonpickle.decode(flow_pickle)
def oauth2_callback(request):
@@ -170,7 +170,10 @@ def oauth2_authorize(request):
A redirect to Google OAuth2 Authorization.
"""
return_url = request.GET.get('return_url', None)
if not return_url:
return_url = request.META.get('HTTP_REFERER', '/')
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
# Model storage (but not session storage) requires a logged in user
if django_util.oauth2_settings.storage_model:
if not request.user.is_authenticated():
@@ -178,13 +181,11 @@ def oauth2_authorize(request):
settings.LOGIN_URL, parse.quote(request.get_full_path())))
# This checks for the case where we ended up here because of a logged
# out user but we had credentials for it in the first place
elif get_storage(request).get() is not None:
return redirect(return_url)
else:
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
if user_oauth.has_credentials():
return redirect(return_url)
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
if not return_url:
return_url = request.META.get('HTTP_REFERER', '/')
flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
auth_url = flow.step1_get_authorize_url()
return shortcuts.redirect(auth_url)

View File

@@ -179,16 +179,14 @@ try:
except ImportError: # pragma: NO COVER
raise ImportError('The flask utilities require flask 0.9 or newer.')
import httplib2
import six.moves.http_client as httplib
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client.contrib import dictionary_storage
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
_DEFAULT_SCOPES = ('email',)
_CREDENTIALS_KEY = 'google_oauth2_credentials'
_FLOW_KEY = 'google_oauth2_flow_{0}'
@@ -553,4 +551,5 @@ class UserOAuth2(object):
"""
if not self.credentials:
raise ValueError('No credentials available.')
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
return self.credentials.authorize(
transport.get_http_object(*args, **kwargs))

View File

@@ -20,14 +20,12 @@ Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
import logging
import warnings
import httplib2
from six.moves import http_client
from oauth2client import client
from oauth2client.contrib import _metadata
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
logger = logging.getLogger(__name__)
_SCOPES_WARNING = """\
@@ -98,44 +96,40 @@ class AppAssertionCredentials(client.AssertionCredentials):
Returns:
A set of strings containing the canonical list of scopes.
"""
self._retrieve_info(http.request)
self._retrieve_info(http)
return self.scopes
def _retrieve_info(self, http_request):
"""Validates invalid service accounts by retrieving service account info.
def _retrieve_info(self, http):
"""Retrieves service account info for invalid credentials.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make the
request to the metadata server
http: an object to be used to make HTTP requests.
"""
if self.invalid:
info = _metadata.get_service_account_info(
http_request,
http,
service_account=self.service_account_email or 'default')
self.invalid = False
self.service_account_email = info['email']
self.scopes = info['scopes']
def _refresh(self, http_request):
"""Refreshes the access_token.
def _refresh(self, http):
"""Refreshes the access token.
Skip all the storage hoops and just refresh using the API.
Args:
http_request: callable, a callable that matches the method
signature of httplib2.Http.request, used to make
the refresh request.
http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
"""
try:
self._retrieve_info(http_request)
self._retrieve_info(http)
self.access_token, self.token_expiry = _metadata.get_token(
http_request, service_account=self.service_account_email)
except httplib2.HttpLib2Error as e:
raise client.HttpAccessTokenRefreshError(str(e))
http, service_account=self.service_account_email)
except http_client.HTTPException as err:
raise client.HttpAccessTokenRefreshError(str(err))
@property
def serialization_data(self):

View File

@@ -24,9 +24,6 @@ import keyring
from oauth2client import client
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
class Storage(client.Storage):
"""Store and retrieve a single credential to and from the keyring.

View File

@@ -20,12 +20,7 @@ import hmac
import time
from oauth2client import _helpers
from oauth2client import util
__authors__ = [
'"Doug Coker" <dcoker@google.com>',
'"Joe Gregorio" <jcgregorio@google.com>',
]
# Delimiter character
DELIMITER = b':'
@@ -34,7 +29,7 @@ DELIMITER = b':'
DEFAULT_TIMEOUT_SECS = 60 * 60
@util.positional(2)
@_helpers.positional(2)
def generate_token(key, user_id, action_id='', when=None):
"""Generates a URL-safe token for the given user, action, time tuple.
@@ -62,7 +57,7 @@ def generate_token(key, user_id, action_id='', when=None):
return token
@util.positional(3)
@_helpers.positional(3)
def validate_token(key, token, user_id, action_id="", current_time=None):
"""Validates that the given token authorizes the user for the action.

View File

@@ -21,16 +21,10 @@ credentials.
import os
import threading
from oauth2client import _helpers
from oauth2client import client
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
class CredentialsFileSymbolicLinkError(Exception):
"""Credentials files must not be symbolic links."""
class Storage(client.Storage):
"""Store and retrieve a single credential to and from a file."""
@@ -38,11 +32,6 @@ class Storage(client.Storage):
super(Storage, self).__init__(lock=threading.Lock())
self._filename = filename
def _validate_file(self):
if os.path.islink(self._filename):
raise CredentialsFileSymbolicLinkError(
'File: {0} is a symbolic link.'.format(self._filename))
def locked_get(self):
"""Retrieve Credential from file.
@@ -50,10 +39,10 @@ class Storage(client.Storage):
oauth2client.client.Credentials
Raises:
CredentialsFileSymbolicLinkError if the file is a symbolic link.
IOError if the file is a symbolic link.
"""
credentials = None
self._validate_file()
_helpers.validate_file(self._filename)
try:
f = open(self._filename, 'rb')
content = f.read()
@@ -89,10 +78,10 @@ class Storage(client.Storage):
credentials: Credentials, the credentials to store.
Raises:
CredentialsFileSymbolicLinkError if the file is a symbolic link.
IOError if the file is a symbolic link.
"""
self._create_file_if_needed()
self._validate_file()
_helpers.validate_file(self._filename)
f = open(self._filename, 'w')
f.write(credentials.to_json())
f.close()

View File

@@ -25,7 +25,6 @@ from oauth2client import _helpers
from oauth2client import client
from oauth2client import crypt
from oauth2client import transport
from oauth2client import util
_PASSWORD_DEFAULT = 'notasecret'
@@ -110,7 +109,7 @@ class ServiceAccountCredentials(client.AssertionCredentials):
self._service_account_email = service_account_email
self._signer = signer
self._scopes = util.scopes_to_string(scopes)
self._scopes = _helpers.scopes_to_string(scopes)
self._private_key_id = private_key_id
self.client_id = client_id
self._user_agent = user_agent
@@ -650,9 +649,22 @@ class _JWTAccessCredentials(ServiceAccountCredentials):
return result
def refresh(self, http):
"""Refreshes the access_token.
The HTTP object is unused since no request needs to be made to
get a new token, it can just be generated locally.
Args:
http: unused HTTP object
"""
self._refresh(None)
def _refresh(self, http_request):
def _refresh(self, http):
"""Refreshes the access_token.
Args:
http: unused HTTP object
"""
self.access_token, self.token_expiry = self._create_token()
def _create_token(self, additional_claims=None):

View File

@@ -30,11 +30,10 @@ from six.moves import http_client
from six.moves import input
from six.moves import urllib
from oauth2client import _helpers
from oauth2client import client
from oauth2client import util
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
__all__ = ['argparser', 'run_flow', 'message_if_missing']
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
@@ -123,22 +122,22 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if an error occurred.
"""
self.send_response(http_client.OK)
self.send_header("Content-type", "text/html")
self.send_header('Content-type', 'text/html')
self.end_headers()
query = self.path.split('?', 1)[-1]
query = dict(urllib.parse.parse_qsl(query))
parts = urllib.parse.urlparse(self.path)
query = _helpers.parse_unique_urlencoded(parts.query)
self.server.query_params = query
self.wfile.write(
b"<html><head><title>Authentication Status</title></head>")
b'<html><head><title>Authentication Status</title></head>')
self.wfile.write(
b"<body><p>The authentication flow has completed.</p>")
self.wfile.write(b"</body></html>")
b'<body><p>The authentication flow has completed.</p>')
self.wfile.write(b'</body></html>')
def log_message(self, format, *args):
"""Do not log messages to stdout while running as cmd. line program."""
@util.positional(3)
@_helpers.positional(3)
def run_flow(flow, storage, flags=None, http=None):
"""Core code for a command-line application.

View File

@@ -18,7 +18,7 @@ import httplib2
import six
from six.moves import http_client
from oauth2client._helpers import _to_bytes
from oauth2client import _helpers
_LOGGER = logging.getLogger(__name__)
@@ -58,13 +58,19 @@ def get_cached_http():
return _CACHED_HTTP
def get_http_object():
def get_http_object(*args, **kwargs):
"""Return a new HTTP object.
Args:
*args: tuple, The positional arguments to be passed when
contructing a new HTTP object.
**kwargs: dict, The keyword arguments to be passed when
contructing a new HTTP object.
Returns:
httplib2.Http, an HTTP object.
"""
return httplib2.Http()
return httplib2.Http(*args, **kwargs)
def _initialize_headers(headers):
@@ -121,7 +127,7 @@ def clean_headers(headers):
k = str(k)
if not isinstance(v, six.binary_type):
v = str(v)
clean[_to_bytes(k)] = _to_bytes(v)
clean[_helpers._to_bytes(k)] = _helpers._to_bytes(v)
except UnicodeEncodeError:
from oauth2client.client import NonAsciiHeaderError
raise NonAsciiHeaderError(k, ': ', v)
@@ -164,9 +170,9 @@ def wrap_http_for_auth(credentials, http):
_STREAM_PROPERTIES):
body_stream_position = body.tell()
resp, content = orig_request_method(uri, method, body,
clean_headers(headers),
redirections, connection_type)
resp, content = request(orig_request_method, uri, method, body,
clean_headers(headers),
redirections, connection_type)
# 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.
@@ -182,9 +188,9 @@ def wrap_http_for_auth(credentials, http):
if body_stream_position is not None:
body.seek(body_stream_position)
resp, content = orig_request_method(uri, method, body,
clean_headers(headers),
redirections, connection_type)
resp, content = request(orig_request_method, uri, method, body,
clean_headers(headers),
redirections, connection_type)
return resp, content
@@ -192,7 +198,7 @@ def wrap_http_for_auth(credentials, http):
http.request = new_request
# Set credentials as a property of the request method.
setattr(http.request, 'credentials', credentials)
http.request.credentials = credentials
def wrap_http_for_jwt_access(credentials, http):
@@ -222,9 +228,9 @@ def wrap_http_for_jwt_access(credentials, http):
if (credentials.access_token is None or
credentials.access_token_expired):
credentials.refresh(None)
return authenticated_request_method(uri, method, body,
headers, redirections,
connection_type)
return request(authenticated_request_method, uri,
method, body, headers, redirections,
connection_type)
else:
# If we don't have an 'aud' (audience) claim,
# create a 1-time token with the uri root as the audience
@@ -234,12 +240,46 @@ def wrap_http_for_jwt_access(credentials, http):
token, unused_expiry = credentials._create_token({'aud': uri_root})
headers['Authorization'] = 'Bearer ' + token
return orig_request_method(uri, method, body,
clean_headers(headers),
redirections, connection_type)
return request(orig_request_method, uri, method, body,
clean_headers(headers),
redirections, connection_type)
# Replace the request method with our own closure.
http.request = new_request
# Set credentials as a property of the request method.
http.request.credentials = credentials
def request(http, uri, method='GET', body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
connection_type=None):
"""Make an HTTP request with an HTTP object and arguments.
Args:
http: httplib2.Http, an http object to be used to make requests.
uri: string, The URI to be requested.
method: string, The HTTP method to use for the request. Defaults
to 'GET'.
body: string, The payload / body in HTTP request. By default
there is no payload.
headers: dict, Key-value pairs of request headers. By default
there are no headers.
redirections: int, The number of allowed 203 redirects for
the request. Defaults to 5.
connection_type: httplib.HTTPConnection, a subclass to be used for
establishing connection. If not set, the type
will be determined from the ``uri``.
Returns:
tuple, a pair of a httplib2.Response with the status code and other
headers and the bytes of the content returned.
"""
# NOTE: Allowing http or http.request is temporary (See Issue 601).
http_callable = getattr(http, 'request', http)
return http_callable(uri, method=method, body=body, headers=headers,
redirections=redirections,
connection_type=connection_type)
_CACHED_HTTP = httplib2.Http(MemoryCache())

11
src/project-apis.txt Normal file
View File

@@ -0,0 +1,11 @@
admin.googleapis.com
appsactivity.googleapis.com
calendar-json.googleapis.com
classroom.googleapis.com
contacts.googleapis.com
drive
gmail.googleapis.com
groupssettings.googleapis.com
licensing.googleapis.com
plus.googleapis.com
siteverification.googleapis.com

80
src/utils.py Normal file
View File

@@ -0,0 +1,80 @@
from var import GM_Globals, GM_WINDOWS, GM_SYS_ENCODING
import collections
import re
import sys
from htmlentitydefs import name2codepoint
from HTMLParser import HTMLParser
def convertUTF8(data):
if isinstance(data, str):
return data
if isinstance(data, unicode):
if GM_Globals[GM_WINDOWS]:
return data
return data.encode(GM_Globals[GM_SYS_ENCODING])
if isinstance(data, collections.Mapping):
return dict(map(convertUTF8, data.iteritems()))
if isinstance(data, collections.Iterable):
return type(data)(map(convertUTF8, data))
return data
class _DeHTMLParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.__text = []
def handle_data(self, data):
self.__text.append(data)
def handle_charref(self, name):
self.__text.append(unichr(int(name[1:], 16)) if name.startswith('x') else unichr(int(name)))
def handle_entityref(self, name):
cp = name2codepoint.get(name)
if cp:
self.__text.append(unichr(cp))
else:
self.__text.append(u'&'+name)
def handle_starttag(self, tag, attrs):
if tag == 'p':
self.__text.append('\n\n')
elif tag == 'br':
self.__text.append('\n')
elif tag == 'a':
for attr in attrs:
if attr[0] == 'href':
self.__text.append('({0}) '.format(attr[1]))
break
elif tag == 'div':
if not attrs:
self.__text.append('\n')
elif tag in ['http:', 'https']:
self.__text.append(' ({0}//{1}) '.format(tag, attrs[0][0]))
def handle_startendtag(self, tag, attrs):
if tag == 'br':
self.__text.append('\n\n')
def text(self):
return re.sub(r'\n{2}\n+', '\n\n', re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
def dehtml(text):
try:
parser = _DeHTMLParser()
parser.feed(text.encode(u'utf-8'))
parser.close()
return parser.text()
except:
from traceback import print_exc
print_exc(file=sys.stderr)
return text
def indentMultiLineText(message, n=0):
return message.replace(u'\n', u'\n{0}'.format(u' '*n)).rstrip()
def formatMilliSeconds(millis):
seconds, millis = divmod(millis, 1000)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
return u'%02d:%02d:%02d' % (hours, minutes, seconds)

686
src/var.py Normal file
View File

@@ -0,0 +1,686 @@
import os
import sys
import platform
import re
gam_author = u'Jay Lee <jay0lee@gmail.com>'
gam_version = u'4.1'
gam_license = u'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = u'http://git.io/gam'
GAM_INFO = u'GAM {0} - {1} / {2} / Python {3}.{4}.{5} {6} / {7} {8} /'.format(gam_version, GAM_URL,
gam_author, sys.version_info[0], sys.version_info[1], sys.version_info[2], sys.version_info[3],
platform.platform(), platform.machine())
GAM_RELEASES = u'https://github.com/jay0lee/GAM/releases'
GAM_WIKI = u'https://github.com/jay0lee/GAM/wiki'
GAM_ALL_RELEASES = u'https://api.github.com/repos/jay0lee/GAM/releases'
GAM_LATEST_RELEASE = GAM_ALL_RELEASES+u'/latest'
TRUE = u'true'
FALSE = u'false'
true_values = [u'on', u'yes', u'enabled', u'true', u'1']
false_values = [u'off', u'no', u'disabled', u'false', u'0']
usergroup_types = [u'user', u'users', u'group', u'ou', u'org',
u'ou_and_children', u'ou_and_child', u'query',
u'license', u'licenses', u'licence', u'licences', u'file', u'csv', u'all',
u'cros']
ERROR = u'ERROR'
ERROR_PREFIX = ERROR+u': '
WARNING = u'WARNING'
WARNING_PREFIX = WARNING+u': '
DEFAULT_CHARSET = [u'mbcs', u'utf-8'][os.name != u'nt']
ONE_KILO_BYTES = 1000
ONE_MEGA_BYTES = 1000000
ONE_GIGA_BYTES = 1000000000
FN_CLIENT_SECRETS_JSON = u'client_secrets.json'
FN_EXTRA_ARGS_TXT = u'extra-args.txt'
FN_LAST_UPDATE_CHECK_TXT = u'lastupdatecheck.txt'
FN_OAUTH2SERVICE_JSON = u'oauth2service.json'
FN_OAUTH2_TXT = u'oauth2.txt'
MY_CUSTOMER = u'my_customer'
SKUS = {
u'Google-Apps-For-Business': {
u'product': u'Google-Apps', u'aliases': [u'gafb', u'gafw', u'basic', u'gsuite-basic']},
u'Google-Apps-For-Government': {
u'product': u'Google-Apps', u'aliases': [u'gafg', u'gsuite-government']},
u'Google-Apps-For-Postini': {
u'product': u'Google-Apps', u'aliases': [u'gams', u'postini', u'gsuite-gams']},
u'Google-Apps-Lite': {
u'product': u'Google-Apps', u'aliases': [u'gal', u'lite', u'gsuite-lite']},
u'Google-Apps-Unlimited': {
u'product': u'Google-Apps', u'aliases': [u'gau', u'unlimited', u'gsuite-business']},
u'Google-Drive-storage-20GB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-20gb', u'drive20gb', u'20gb']},
u'Google-Drive-storage-50GB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-50gb', u'drive50gb', u'50gb']},
u'Google-Drive-storage-200GB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-200gb', u'drive200gb', u'200gb']},
u'Google-Drive-storage-400GB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-400gb', u'drive400gb', u'400gb']},
u'Google-Drive-storage-1TB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-1tb', u'drive1tb', u'1tb']},
u'Google-Drive-storage-2TB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-2tb', u'drive2tb', u'2tb']},
u'Google-Drive-storage-4TB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-4tb', u'drive4tb', u'4tb']},
u'Google-Drive-storage-8TB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-8tb', u'drive8tb', u'8tb']},
u'Google-Drive-storage-16TB': {
u'product': u'Google-Drive-storage', u'aliases': [u'drive-16tb', u'drive16tb', u'16tb']},
u'Google-Vault': {
u'product': u'Google-Vault', u'aliases': [u'vault']},
u'Google-Vault-Former-Employee': {
u'product': u'Google-Vault', u'aliases': [u'vfe']},
u'Google-Coordinate': {
u'product': u'Google-Coordinate', u'aliases': [u'coordinate']}
}
API_VER_MAPPING = {
u'appsactivity': u'v1',
u'calendar': u'v3',
u'classroom': u'v1',
u'cloudprint': u'v2',
u'datatransfer': u'datatransfer_v1',
u'directory': u'directory_v1',
u'drive': u'v2',
u'email-settings': u'v2',
u'gmail': u'v1',
u'groupssettings': u'v1',
u'licensing': u'v1',
u'oauth2': u'v2',
u'plus': u'v1',
u'reports': u'reports_v1',
u'siteVerification': u'v1',
}
API_SCOPE_MAPPING = {
u'appsactivity': [u'https://www.googleapis.com/auth/activity',
u'https://www.googleapis.com/auth/drive'],
u'calendar': [u'https://www.googleapis.com/auth/calendar',],
u'drive': [u'https://www.googleapis.com/auth/drive',],
u'gmail': [u'https://mail.google.com/',
u'https://www.googleapis.com/auth/gmail.settings.basic',
u'https://www.googleapis.com/auth/gmail.settings.sharing',],
u'plus': [u'https://www.googleapis.com/auth/plus.me',],
}
ADDRESS_FIELDS_PRINT_ORDER = [u'contactName', u'organizationName',
u'addressLine1', u'addressLine2', u'addressLine3', u'locality',
u'region', u'postalCode', u'countryCode']
ADDRESS_FIELDS_ARGUMENT_MAP = {
u'contact': u'contactName', u'contactname': u'contactName',
u'name': u'organizationName', u'organizationname': u'organizationName',
u'address1': u'addressLine1', u'addressline1': u'addressLine1',
u'address2': u'addressLine2', u'addressline2': u'addressLine2',
u'address3': u'addressLine3', u'addressline3': u'addressLine3',
u'locality': u'locality',
u'region': u'region',
u'postalcode': u'postalCode',
u'country': u'countryCode', u'countrycode': u'countryCode',
}
SERVICE_NAME_CHOICES_MAP = {
u'drive': u'Drive and Docs',
u'drive and docs': u'Drive and Docs',
u'googledrive': u'Drive and Docs',
u'gdrive': u'Drive and Docs',
}
PRINTJOB_ASCENDINGORDER_MAP = {
u'createtime': u'CREATE_TIME',
u'status': u'STATUS',
u'title': u'TITLE',
}
PRINTJOB_DESCENDINGORDER_MAP = {
u'CREATE_TIME': u'CREATE_TIME_DESC',
u'STATUS': u'STATUS_DESC',
u'TITLE': u'TITLE_DESC',
}
PRINTJOBS_DEFAULT_JOB_LIMIT = 25
PRINTJOBS_DEFAULT_MAX_RESULTS = 100
CALENDAR_REMINDER_METHODS = [u'email', u'sms', u'popup',]
CALENDAR_NOTIFICATION_METHODS = [u'email', u'sms',]
CALENDAR_NOTIFICATION_TYPES_MAP = {
u'eventcreation': u'eventCreation',
u'eventchange': u'eventChange',
u'eventcancellation': u'eventCancellation',
u'eventresponse': u'eventResponse',
u'agenda': u'agenda',
}
DRIVEFILE_FIELDS_CHOICES_MAP = {
u'alternatelink': u'alternateLink',
u'appdatacontents': u'appDataContents',
u'cancomment': u'canComment',
u'canreadrevisions': u'canReadRevisions',
u'copyable': u'copyable',
u'createddate': u'createdDate',
u'createdtime': u'createdDate',
u'description': u'description',
u'editable': u'editable',
u'explicitlytrashed': u'explicitlyTrashed',
u'fileextension': u'fileExtension',
u'filesize': u'fileSize',
u'foldercolorrgb': u'folderColorRgb',
u'fullfileextension': u'fullFileExtension',
u'headrevisionid': u'headRevisionId',
u'iconlink': u'iconLink',
u'id': u'id',
u'lastmodifyinguser': u'lastModifyingUser',
u'lastmodifyingusername': u'lastModifyingUserName',
u'lastviewedbyme': u'lastViewedByMeDate',
u'lastviewedbymedate': u'lastViewedByMeDate',
u'lastviewedbymetime': u'lastViewedByMeDate',
u'lastviewedbyuser': u'lastViewedByMeDate',
u'md5': u'md5Checksum',
u'md5checksum': u'md5Checksum',
u'md5sum': u'md5Checksum',
u'mime': u'mimeType',
u'mimetype': u'mimeType',
u'modifiedbyme': u'modifiedByMeDate',
u'modifiedbymedate': u'modifiedByMeDate',
u'modifiedbymetime': u'modifiedByMeDate',
u'modifiedbyuser': u'modifiedByMeDate',
u'modifieddate': u'modifiedDate',
u'modifiedtime': u'modifiedDate',
u'name': u'title',
u'originalfilename': u'originalFilename',
u'ownedbyme': u'ownedByMe',
u'ownernames': u'ownerNames',
u'owners': u'owners',
u'parents': u'parents',
u'permissions': u'permissions',
u'quotabytesused': u'quotaBytesUsed',
u'quotaused': u'quotaBytesUsed',
u'shareable': u'shareable',
u'shared': u'shared',
u'sharedwithmedate': u'sharedWithMeDate',
u'sharedwithmetime': u'sharedWithMeDate',
u'sharinguser': u'sharingUser',
u'spaces': u'spaces',
u'thumbnaillink': u'thumbnailLink',
u'title': u'title',
u'userpermission': u'userPermission',
u'version': u'version',
u'viewedbyme': u'labels(viewed)',
u'viewedbymedate': u'lastViewedByMeDate',
u'viewedbymetime': u'lastViewedByMeDate',
u'viewerscancopycontent': u'labels(restricted)',
u'webcontentlink': u'webContentLink',
u'webviewlink': u'webViewLink',
u'writerscanshare': u'writersCanShare',
}
DRIVEFILE_LABEL_CHOICES_MAP = {
u'restricted': u'restricted',
u'restrict': u'restricted',
u'starred': u'starred',
u'star': u'starred',
u'trashed': u'trashed',
u'trash': u'trashed',
u'viewed': u'viewed',
u'view': u'viewed',
}
DRIVEFILE_ORDERBY_CHOICES_MAP = {
u'createddate': u'createdDate',
u'folder': u'folder',
u'lastviewedbyme': u'lastViewedByMeDate',
u'lastviewedbymedate': u'lastViewedByMeDate',
u'lastviewedbyuser': u'lastViewedByMeDate',
u'modifiedbyme': u'modifiedByMeDate',
u'modifiedbymedate': u'modifiedByMeDate',
u'modifiedbyuser': u'modifiedByMeDate',
u'modifieddate': u'modifiedDate',
u'name': u'title',
u'quotabytesused': u'quotaBytesUsed',
u'quotaused': u'quotaBytesUsed',
u'recency': u'recency',
u'sharedwithmedate': u'sharedWithMeDate',
u'starred': u'starred',
u'title': u'title',
u'viewedbymedate': u'lastViewedByMeDate',
}
DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP = {u'delete': u'purging',
u'trash': u'trashing', u'untrash': u'untrashing',}
DRIVEFILE_LABEL_CHOICES_MAP = {
u'restricted': u'restricted',
u'restrict': u'restricted',
u'starred': u'starred',
u'star': u'starred',
u'trashed': u'trashed',
u'trash': u'trashed',
u'viewed': u'viewed',
u'view': u'viewed',
}
APPLICATION_VND_GOOGLE_APPS = u'application/vnd.google-apps.'
MIMETYPE_GA_DOCUMENT = APPLICATION_VND_GOOGLE_APPS+u'document'
MIMETYPE_GA_DRAWING = APPLICATION_VND_GOOGLE_APPS+u'drawing'
MIMETYPE_GA_FOLDER = APPLICATION_VND_GOOGLE_APPS+u'folder'
MIMETYPE_GA_FORM = APPLICATION_VND_GOOGLE_APPS+u'form'
MIMETYPE_GA_FUSIONTABLE = APPLICATION_VND_GOOGLE_APPS+u'fusiontable'
MIMETYPE_GA_MAP = APPLICATION_VND_GOOGLE_APPS+u'map'
MIMETYPE_GA_PRESENTATION = APPLICATION_VND_GOOGLE_APPS+u'presentation'
MIMETYPE_GA_SCRIPT = APPLICATION_VND_GOOGLE_APPS+u'script'
MIMETYPE_GA_SITES = APPLICATION_VND_GOOGLE_APPS+u'sites'
MIMETYPE_GA_SPREADSHEET = APPLICATION_VND_GOOGLE_APPS+u'spreadsheet'
MIMETYPE_CHOICES_MAP = {
u'gdoc': MIMETYPE_GA_DOCUMENT,
u'gdocument': MIMETYPE_GA_DOCUMENT,
u'gdrawing': MIMETYPE_GA_DRAWING,
u'gfolder': MIMETYPE_GA_FOLDER,
u'gdirectory': MIMETYPE_GA_FOLDER,
u'gform': MIMETYPE_GA_FORM,
u'gfusion': MIMETYPE_GA_FUSIONTABLE,
u'gpresentation': MIMETYPE_GA_PRESENTATION,
u'gscript': MIMETYPE_GA_SCRIPT,
u'gsite': MIMETYPE_GA_SITES,
u'gsheet': MIMETYPE_GA_SPREADSHEET,
u'gspreadsheet': MIMETYPE_GA_SPREADSHEET,
}
DFA_CONVERT = u'convert'
DFA_LOCALFILEPATH = u'localFilepath'
DFA_LOCALFILENAME = u'localFilename'
DFA_LOCALMIMETYPE = u'localMimeType'
DFA_OCR = u'ocr'
DFA_OCRLANGUAGE = u'ocrLanguage'
DFA_PARENTQUERY = u'parentQuery'
DOCUMENT_FORMATS_MAP = {
u'csv': [{u'mime': u'text/csv', u'ext': u'.csv'}],
u'html': [{u'mime': u'text/html', u'ext': u'.html'}],
u'txt': [{u'mime': u'text/plain', u'ext': u'.txt'}],
u'tsv': [{u'mime': u'text/tsv', u'ext': u'.tsv'}],
u'jpeg': [{u'mime': u'image/jpeg', u'ext': u'.jpeg'}],
u'jpg': [{u'mime': u'image/jpeg', u'ext': u'.jpg'}],
u'png': [{u'mime': u'image/png', u'ext': u'.png'}],
u'svg': [{u'mime': u'image/svg+xml', u'ext': u'.svg'}],
u'pdf': [{u'mime': u'application/pdf', u'ext': u'.pdf'}],
u'rtf': [{u'mime': u'application/rtf', u'ext': u'.rtf'}],
u'zip': [{u'mime': u'application/zip', u'ext': u'.zip'}],
u'pptx': [{u'mime': u'application/vnd.openxmlformats-officedocument.presentationml.presentation', u'ext': u'.pptx'}],
u'xlsx': [{u'mime': u'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', u'ext': u'.xlsx'}],
u'docx': [{u'mime': u'application/vnd.openxmlformats-officedocument.wordprocessingml.document', u'ext': u'.docx'}],
u'ms': [{u'mime': u'application/vnd.openxmlformats-officedocument.presentationml.presentation', u'ext': u'.pptx'},
{u'mime': u'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', u'ext': u'.xlsx'},
{u'mime': u'application/vnd.openxmlformats-officedocument.wordprocessingml.document', u'ext': u'.docx'}],
u'microsoft': [{u'mime': u'application/vnd.openxmlformats-officedocument.presentationml.presentation', u'ext': u'.pptx'},
{u'mime': u'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', u'ext': u'.xlsx'},
{u'mime': u'application/vnd.openxmlformats-officedocument.wordprocessingml.document', u'ext': u'.docx'}],
u'micro$oft': [{u'mime': u'application/vnd.openxmlformats-officedocument.presentationml.presentation', u'ext': u'.pptx'},
{u'mime': u'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', u'ext': u'.xlsx'},
{u'mime': u'application/vnd.openxmlformats-officedocument.wordprocessingml.document', u'ext': u'.docx'}],
u'odt': [{u'mime': u'application/vnd.oasis.opendocument.text', u'ext': u'.odt'}],
u'ods': [{u'mime': u'application/x-vnd.oasis.opendocument.spreadsheet', u'ext': u'.ods'}],
u'openoffice': [{u'mime': u'application/vnd.oasis.opendocument.text', u'ext': u'.odt'},
{u'mime': u'application/x-vnd.oasis.opendocument.spreadsheet', u'ext': u'.ods'}],
}
EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP = {
u'ARCHIVE': u'archive',
u'DELETE': u'trash',
u'KEEP': u'leaveInInBox',
u'MARK_READ': u'markRead',
u'archive': u'ARCHIVE',
u'trash': u'DELETE',
u'leaveInInbox': u'KEEP',
u'markRead': u'MARK_READ',
}
EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP = {
u'archive': u'archive',
u'deleteforever': u'deleteForever',
u'trash': u'trash',
}
EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES = [u'0', u'1000', u'2000', u'5000', u'10000']
EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP = {
u'allmail': u'allMail',
u'fromnowon': u'fromNowOn',
u'mailfromnowon': u'fromNowOn',
u'newmail': u'fromNowOn',
}
EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP = {
u'archive': u'archive',
u'delete': u'trash',
u'keep': u'leaveInInbox',
u'leaveininbox': u'leaveInInbox',
u'markread': u'markRead',
u'trash': u'trash',
}
RT_PATTERN = re.compile(r'(?s){RT}.*?{(.+?)}.*?{/RT}')
RT_OPEN_PATTERN = re.compile(r'{RT}')
RT_CLOSE_PATTERN = re.compile(r'{/RT}')
RT_STRIP_PATTERN = re.compile(r'(?s){RT}.*?{/RT}')
RT_TAG_REPLACE_PATTERN = re.compile(r'{(.*?)}')
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
u'IMPORTANT': u'important',
u'STARRED': u'star',
u'TRASH': u'trash',
}
FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP = {
u'IMPORTANT': u'notimportant',
u'UNREAD': u'markread',
u'INBOX': u'archive',
u'SPAM': u'neverspam',
}
FILTER_CRITERIA_CHOICES_MAP = {
u'excludechats': u'excludeChats',
u'from': u'from',
u'hasattachment': u'hasAttachment',
u'haswords': u'query',
u'musthaveattachment': u'hasAttachment',
u'negatedquery': u'negatedQuery',
u'nowords': u'negatedQuery',
u'query': u'query',
u'size': u'size',
u'subject': u'subject',
u'to': u'to',
}
FILTER_ACTION_CHOICES = [u'archive', u'forward', u'important', u'label',
u'markread', u'neverspam', u'notimportant', u'star', u'trash',]
CROS_ARGUMENT_TO_PROPERTY_MAP = {
u'activetimeranges': [u'activeTimeRanges.activeTime', u'activeTimeRanges.date'],
u'annotatedassetid': [u'annotatedAssetId',],
u'annotatedlocation': [u'annotatedLocation',],
u'annotateduser': [u'annotatedUser',],
u'asset': [u'annotatedAssetId',],
u'assetid': [u'annotatedAssetId',],
u'bootmode': [u'bootMode',],
u'deviceid': [u'deviceId',],
u'ethernetmacaddress': [u'ethernetMacAddress',],
u'firmwareversion': [u'firmwareVersion',],
u'lastenrollmenttime': [u'lastEnrollmentTime',],
u'lastsync': [u'lastSync',],
u'location': [u'annotatedLocation',],
u'macaddress': [u'macAddress',],
u'meid': [u'meid',],
u'model': [u'model',],
u'notes': [u'notes',],
u'ordernumber': [u'orderNumber',],
u'org': [u'orgUnitPath',],
u'orgunitpath': [u'orgUnitPath',],
u'osversion': [u'osVersion',],
u'ou': [u'orgUnitPath',],
u'platformversion': [u'platformVersion',],
u'recentusers': [u'recentUsers.email', u'recentUsers.type'],
u'serialnumber': [u'serialNumber',],
u'status': [u'status',],
u'supportenddate': [u'supportEndDate',],
u'tag': [u'annotatedAssetId',],
u'timeranges': [u'activeTimeRanges.activeTime', u'activeTimeRanges.date'],
u'user': [u'annotatedUser',],
u'willautorenew': [u'willAutoRenew',],
}
CROS_BASIC_FIELDS_LIST = [u'deviceId', u'annotatedAssetId', u'annotatedLocation', u'annotatedUser', u'lastSync', u'notes', u'serialNumber', u'status']
CROS_SCALAR_PROPERTY_PRINT_ORDER = [
u'orgUnitPath',
u'annotatedAssetId',
u'annotatedLocation',
u'annotatedUser',
u'lastSync',
u'notes',
u'serialNumber',
u'status',
u'model',
u'firmwareVersion',
u'platformVersion',
u'osVersion',
u'bootMode',
u'meid',
u'ethernetMacAddress',
u'macAddress',
u'lastEnrollmentTime',
u'orderNumber',
u'supportEndDate',
u'willAutoRenew',
]
#
# Global variables
#
# The following GM_XXX constants are arbitrary but must be unique
# Most errors print a message and bail out with a return code
# Some commands want to set a non-zero return code but not bail
GM_SYSEXITRC = u'sxrc'
# Path to gam
GM_GAM_PATH = u'gpth'
# Are we on Windows?
GM_WINDOWS = u'wndo'
# Encodings
GM_SYS_ENCODING = u'syen'
# Extra arguments to pass to GAPI functions
GM_EXTRA_ARGS_DICT = u'exad'
# Current API user
GM_CURRENT_API_USER = u'capu'
# Current API scope
GM_CURRENT_API_SCOPES = u'scoc'
# Values retrieved from oauth2service.json
GM_OAUTH2SERVICE_JSON_DATA = u'oajd'
GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID = u'oaci'
# File containing time of last GAM update check
GM_LAST_UPDATE_CHECK_TXT = u'lupc'
# Dictionary mapping OrgUnit ID to Name
GM_MAP_ORGUNIT_ID_TO_NAME = u'oi2n'
# Dictionary mapping Role ID to Name
GM_MAP_ROLE_ID_TO_NAME = u'ri2n'
# Dictionary mapping Role Name to ID
GM_MAP_ROLE_NAME_TO_ID = u'rn2i'
# Dictionary mapping User ID to Name
GM_MAP_USER_ID_TO_NAME = u'ui2n'
#
GM_Globals = {
GM_SYSEXITRC: 0,
GM_GAM_PATH: os.path.dirname(os.path.realpath(__file__)) if not getattr(sys, u'frozen', False) else os.path.dirname(sys.executable),
GM_WINDOWS: os.name == u'nt',
GM_SYS_ENCODING: DEFAULT_CHARSET,
GM_EXTRA_ARGS_DICT: {u'prettyPrint': False},
GM_CURRENT_API_USER: None,
GM_CURRENT_API_SCOPES: [],
GM_OAUTH2SERVICE_JSON_DATA: None,
GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID: None,
GM_LAST_UPDATE_CHECK_TXT: u'',
GM_MAP_ORGUNIT_ID_TO_NAME: None,
GM_MAP_ROLE_ID_TO_NAME: None,
GM_MAP_ROLE_NAME_TO_ID: None,
GM_MAP_USER_ID_TO_NAME: None,
}
#
# Global variables defined by environment variables/signal files
#
# When retrieving lists of Google Drive activities from API, how many should be retrieved in each chunk
GC_ACTIVITY_MAX_RESULTS = u'activity_max_results'
# Automatically generate gam batch command if number of users specified in gam users xxx command exceeds this number
# Default: 0, don't automatically generate gam batch commands
GC_AUTO_BATCH_MIN = u'auto_batch_min'
# When processing items in batches, how many should be processed in each batch
GC_BATCH_SIZE = u'batch_size'
# GAM cache directory. If no_cache is specified, this variable will be set to None
GC_CACHE_DIR = u'cache_dir'
# Character set of batch, csv, data files
GC_CHARSET = u'charset'
# Path to client_secrets.json
GC_CLIENT_SECRETS_JSON = u'client_secrets_json'
# GAM config directory containing client_secrets.json, oauth2.txt, oauth2service.json, extra_args.txt
GC_CONFIG_DIR = u'config_dir'
# custmerId from gam.cfg or retrieved from Google
GC_CUSTOMER_ID = u'customer_id'
# If debug_level > 0: extra_args[u'prettyPrint'] = True, httplib2.debuglevel = gam_debug_level, appsObj.debug = True
GC_DEBUG_LEVEL = u'debug_level'
# When retrieving lists of ChromeOS/Mobile devices from API, how many should be retrieved in each chunk
GC_DEVICE_MAX_RESULTS = u'device_max_results'
# Domain obtained from gam.cfg or oauth2.txt
GC_DOMAIN = u'domain'
# Google Drive download directory
GC_DRIVE_DIR = u'drive_dir'
# When retrieving lists of Drive files/folders from API, how many should be retrieved in each chunk
GC_DRIVE_MAX_RESULTS = u'drive_max_results'
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
# and doRequestOAuth prints a link and waits for the verification code when oauth2.txt is being created
GC_NO_BROWSER = u'no_browser'
# Disable GAM API caching
GC_NO_CACHE = u'no_cache'
# Disable GAM update check
GC_NO_UPDATE_CHECK = u'no_update_check'
# Disable SSL certificate validation
GC_NO_VERIFY_SSL = u'no_verify_ssl'
# Number of threads for gam batch
GC_NUM_THREADS = u'num_threads'
# Path to oauth2.txt
GC_OAUTH2_TXT = u'oauth2_txt'
# Path to oauth2service.json
GC_OAUTH2SERVICE_JSON = u'oauth2service_json'
# Default section to use for processing
GC_SECTION = u'section'
# Add (n/m) to end of messages if number of items to be processed exceeds this number
GC_SHOW_COUNTS_MIN = u'show_counts_min'
# Enable/disable "Getting ... " messages
GC_SHOW_GETTINGS = u'show_gettings'
# GAM config directory containing json discovery files
GC_SITE_DIR = u'site_dir'
# When retrieving lists of Users from API, how many should be retrieved in each chunk
GC_USER_MAX_RESULTS = u'user_max_results'
GC_Defaults = {
GC_ACTIVITY_MAX_RESULTS: 100,
GC_AUTO_BATCH_MIN: 0,
GC_BATCH_SIZE: 50,
GC_CACHE_DIR: u'',
GC_CHARSET: DEFAULT_CHARSET,
GC_CLIENT_SECRETS_JSON: FN_CLIENT_SECRETS_JSON,
GC_CONFIG_DIR: u'',
GC_CUSTOMER_ID: MY_CUSTOMER,
GC_DEBUG_LEVEL: 0,
GC_DEVICE_MAX_RESULTS: 500,
GC_DOMAIN: u'',
GC_DRIVE_DIR: u'',
GC_DRIVE_MAX_RESULTS: 1000,
GC_NO_BROWSER: FALSE,
GC_NO_CACHE: FALSE,
GC_NO_UPDATE_CHECK: FALSE,
GC_NO_VERIFY_SSL: FALSE,
GC_NUM_THREADS: 25,
GC_OAUTH2_TXT: FN_OAUTH2_TXT,
GC_OAUTH2SERVICE_JSON: FN_OAUTH2SERVICE_JSON,
GC_SECTION: u'',
GC_SHOW_COUNTS_MIN: 0,
GC_SHOW_GETTINGS: TRUE,
GC_SITE_DIR: u'',
GC_USER_MAX_RESULTS: 500,
}
GC_Values = {}
GC_TYPE_BOOLEAN = u'bool'
GC_TYPE_CHOICE = u'choi'
GC_TYPE_DIRECTORY = u'dire'
GC_TYPE_EMAIL = u'emai'
GC_TYPE_FILE = u'file'
GC_TYPE_INTEGER = u'inte'
GC_TYPE_LANGUAGE = u'lang'
GC_TYPE_STRING = u'stri'
GC_VAR_TYPE = u'type'
GC_VAR_LIMITS = u'lmit'
GC_VAR_INFO = {
GC_ACTIVITY_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 500)},
GC_AUTO_BATCH_MIN: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (0, None)},
GC_BATCH_SIZE: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 1000)},
GC_CACHE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY},
GC_CHARSET: {GC_VAR_TYPE: GC_TYPE_STRING},
GC_CLIENT_SECRETS_JSON: {GC_VAR_TYPE: GC_TYPE_FILE},
GC_CONFIG_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY},
GC_CUSTOMER_ID: {GC_VAR_TYPE: GC_TYPE_STRING},
GC_DEBUG_LEVEL: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (0, None)},
GC_DEVICE_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 1000)},
GC_DOMAIN: {GC_VAR_TYPE: GC_TYPE_STRING},
GC_DRIVE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY},
GC_DRIVE_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 1000)},
GC_NO_BROWSER: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_NO_CACHE: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_NO_UPDATE_CHECK: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_NO_VERIFY_SSL: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_NUM_THREADS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, None)},
GC_OAUTH2_TXT: {GC_VAR_TYPE: GC_TYPE_FILE},
GC_OAUTH2SERVICE_JSON: {GC_VAR_TYPE: GC_TYPE_FILE},
GC_SECTION: {GC_VAR_TYPE: GC_TYPE_STRING},
GC_SHOW_COUNTS_MIN: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (0, None)},
GC_SHOW_GETTINGS: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_SITE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY},
GC_USER_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 500)},
}
# Google API constants
NEVER_TIME = u'1970-01-01T00:00:00.000Z'
NEVER_START_DATE = u'1970-01-01'
NEVER_END_DATE = u'1969-12-31'
ROLE_MANAGER = u'MANAGER'
ROLE_MEMBER = u'MEMBER'
ROLE_OWNER = u'OWNER'
ROLE_USER = u'USER'
ROLE_MANAGER_MEMBER = u','.join([ROLE_MANAGER, ROLE_MEMBER])
ROLE_MANAGER_OWNER = u','.join([ROLE_MANAGER, ROLE_OWNER])
ROLE_MANAGER_MEMBER_OWNER = u','.join([ROLE_MANAGER, ROLE_MEMBER, ROLE_OWNER])
ROLE_MEMBER_OWNER = u','.join([ROLE_MEMBER, ROLE_OWNER])
PROJECTION_CHOICES_MAP = {u'basic': u'BASIC', u'full': u'FULL',}
SORTORDER_CHOICES_MAP = {u'ascending': u'ASCENDING', u'descending': u'DESCENDING',}
#
CLEAR_NONE_ARGUMENT = [u'clear', u'none',]
#
MESSAGE_API_ACCESS_CONFIG = u'API access is configured in your Control Panel under: Security-Show more-Advanced settings-Manage API client access'
MESSAGE_API_ACCESS_DENIED = u'API access Denied.\n\nPlease make sure the Client ID: {0} is authorized for the API Scope(s): {1}'
MESSAGE_GAM_EXITING_FOR_UPDATE = u'GAM is now exiting so that you can overwrite this old version with the latest release'
MESSAGE_GAM_OUT_OF_MEMORY = u'GAM has run out of memory. If this is a large G Suite instance, you should use a 64-bit version of GAM on Windows or a 64-bit version of Python on other systems.'
MESSAGE_HEADER_NOT_FOUND_IN_CSV_HEADERS = u'Header "{0}" not found in CSV headers of "{1}".'
MESSAGE_HIT_CONTROL_C_TO_UPDATE = u'\n\nHit CTRL+C to visit the GAM website and download the latest release or wait 15 seconds continue with this boring old version. GAM won\'t bother you with this announcement for 1 week or you can create a file named noupdatecheck.txt in the same location as gam.py or gam.exe and GAM won\'t ever check for updates.'
MESSAGE_INVALID_JSON = u'The file {0} has an invalid format.'
MESSAGE_NO_DISCOVERY_INFORMATION = u'No online discovery doc and {0} does not exist locally'
MESSAGE_NO_PYTHON_SSL = u'You don\'t have the Python SSL module installed so we can\'t verify SSL Certificates. You can fix this by installing the Python SSL module or you can live on the edge and turn SSL validation off by creating a file named noverifyssl.txt in the same location as gam.exe / gam.py'
MESSAGE_NO_TRANSFER_LACK_OF_DISK_SPACE = u'Cowardly refusing to perform migration due to lack of target drive space. Source size: {0}mb Target Free: {1}mb'
MESSAGE_REQUEST_COMPLETED_NO_FILES = u'Request completed but no results/files were returned, try requesting again'
MESSAGE_REQUEST_NOT_COMPLETE = u'Request needs to be completed before downloading, current status is: {0}'
MESSAGE_RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET = u'Results are too large for Google Spreadsheets. Uploading as a regular CSV file.'
MESSAGE_SERVICE_NOT_APPLICABLE = u'Service not applicable for this address: {0}. Please make sure service is enabled for user and run\n\ngam user <user> check serviceaccount\n\nfor further instructions'
MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON = u'Please run\n\ngam create project\ngam user <user> check serviceaccount\n\nto create and configure a service account.'
MESSAGE_OAUTH2SERVICE_JSON_INVALID = u'The file {0} is missing required keys (client_email, client_id or private_key). Please remove it and recreate with the commands:\n\ngam create project\ngam user <user> check serviceaccount'
# oauth errors
OAUTH2_TOKEN_ERRORS = [u'access_denied', u'unauthorized_client: Unauthorized client or scope in request.', u'access_denied: Requested client not authorized.',
u'invalid_grant: Not a valid email.', u'invalid_grant: Invalid email or User ID', u'invalid_grant: Bad Request',
u'invalid_request: Invalid impersonation prn email address.', u'internal_failure: Backend Error']
#
# callGAPI throw reasons
GAPI_BACKEND_ERROR = u'backendError'
GAPI_BAD_REQUEST = u'badRequest'
GAPI_FORBIDDEN = u'forbidden'
GAPI_INTERNAL_ERROR = u'internalError'
GAPI_INVALID = u'invalid'
GAPI_NOT_FOUND = u'notFound'
GAPI_QUOTA_EXCEEDED = u'quotaExceeded'
GAPI_RATE_LIMIT_EXCEEDED = u'rateLimitExceeded'
GAPI_SERVICE_NOT_AVAILABLE = u'serviceNotAvailable'
GAPI_USER_NOT_FOUND = u'userNotFound'
GAPI_USER_RATE_LIMIT_EXCEEDED = u'userRateLimitExceeded'
#
GAPI_DEFAULT_RETRY_REASONS = [GAPI_QUOTA_EXCEEDED, GAPI_RATE_LIMIT_EXCEEDED, GAPI_USER_RATE_LIMIT_EXCEEDED, GAPI_BACKEND_ERROR, GAPI_INTERNAL_ERROR]
GAPI_GMAIL_THROW_REASONS = [GAPI_SERVICE_NOT_AVAILABLE]
GAPI_GPLUS_THROW_REASONS = [GAPI_SERVICE_NOT_AVAILABLE]

View File

@@ -1,3 +1,10 @@
GAM 4.1
- Fix "gam create project"
- project create cleanups by Ross
- Various fixes by Ross
- Improved batch command performance. Some commands will see 2-3x speedups.
- Faster "gam info user" commands via batch license retrieval
GAM 4.03
- Minor fixes by Jay and Ross. Mostly to new install process.

View File

@@ -12,7 +12,7 @@ c:\python27-32\scripts\pyinstaller --clean -F --distpath=gam windows-gam.spec
xcopy LICENSE gam\
xcopy whatsnew.txt gam\
xcopy gam-setup.bat gam\
xcopy gamcommands.txt gam\
xcopy GamCommands.txt gam\
del gam\w9xpopen.exe
"%ProgramFiles%\7-Zip\7z.exe" a -tzip gam-%1-windows.zip gam\ -xr!.svn
@@ -20,7 +20,7 @@ c:\python27-64\scripts\pyinstaller --clean -F --distpath=gam-64 windows-gam.spec
xcopy LICENSE gam-64\
xcopy whatsnew.txt gam-64\
xcopy gam-setup.bat gam-64\
xcopy gamcommands.txt gam-64\
xcopy GamCommands.txt gam-64\
"%ProgramFiles%\7-Zip\7z.exe" a -tzip gam-%1-windows-x64.zip gam-64\ -xr!.svn
set GAMVERSION=%1