1"""
2gspread.auth
3~~~~~~~~~~~~
4
5Simple authentication with OAuth.
6
7"""
8
9import json
10import os
11
12from google.oauth2.credentials import Credentials
13from google.oauth2.service_account import Credentials as ServiceAccountCredentials
14from google_auth_oauthlib.flow import InstalledAppFlow
15
16from .client import Client
17
18DEFAULT_SCOPES = [
19    "https://www.googleapis.com/auth/spreadsheets",
20    "https://www.googleapis.com/auth/drive",
21]
22
23READONLY_SCOPES = [
24    "https://www.googleapis.com/auth/spreadsheets.readonly",
25    "https://www.googleapis.com/auth/drive.readonly",
26]
27
28
29def get_config_dir(config_dir_name="gspread", os_is_windows=os.name == "nt"):
30    r"""Construct a config dir path.
31
32    By default:
33        * `%APPDATA%\gspread` on Windows
34        * `~/.config/gspread` everywhere else
35
36    """
37    if os_is_windows:
38        return os.path.join(os.environ["APPDATA"], config_dir_name)
39    else:
40        return os.path.join(os.path.expanduser("~"), ".config", config_dir_name)
41
42
43DEFAULT_CONFIG_DIR = get_config_dir()
44
45DEFAULT_CREDENTIALS_FILENAME = os.path.join(DEFAULT_CONFIG_DIR, "credentials.json")
46DEFAULT_AUTHORIZED_USER_FILENAME = os.path.join(
47    DEFAULT_CONFIG_DIR, "authorized_user.json"
48)
49DEFAULT_SERVICE_ACCOUNT_FILENAME = os.path.join(
50    DEFAULT_CONFIG_DIR, "service_account.json"
51)
52
53
54def local_server_flow(client_config, scopes, port=0):
55    """Run an OAuth flow using a local server strategy.
56
57    Creates an OAuth flow and runs `google_auth_oauthlib.flow.InstalledAppFlow.run_local_server <https://google-auth-oauthlib.readthedocs.io/en/latest/reference/google_auth_oauthlib.flow.html#google_auth_oauthlib.flow.InstalledAppFlow.run_local_server>`_.
58    This will start a local web server and open the authorization URL in
59    the user’s browser.
60
61    Pass this function to ``flow`` parameter of :meth:`~gspread.oauth` to run
62    a local server flow.
63    """
64    flow = InstalledAppFlow.from_client_config(client_config, scopes)
65    return flow.run_local_server(port=port)
66
67
68def console_flow(client_config, scopes):
69    """Run an OAuth flow using a console strategy.
70
71    Creates an OAuth flow and runs `google_auth_oauthlib.flow.InstalledAppFlow.run_console <https://google-auth-oauthlib.readthedocs.io/en/latest/reference/google_auth_oauthlib.flow.html#google_auth_oauthlib.flow.InstalledAppFlow.run_console>`_.
72
73    Pass this function to ``flow`` parameter of :meth:`~gspread.oauth` to run
74    a console strategy.
75    """
76    flow = InstalledAppFlow.from_client_config(client_config, scopes)
77    return flow.run_console()
78
79
80def load_credentials(filename=DEFAULT_AUTHORIZED_USER_FILENAME):
81    if os.path.exists(filename):
82        return Credentials.from_authorized_user_file(filename)
83
84    return None
85
86
87def store_credentials(creds, filename=DEFAULT_AUTHORIZED_USER_FILENAME, strip="token"):
88    os.makedirs(os.path.dirname(filename), exist_ok=True)
89    with open(filename, "w") as f:
90        f.write(creds.to_json(strip))
91
92
93def oauth(
94    scopes=DEFAULT_SCOPES,
95    flow=local_server_flow,
96    credentials_filename=DEFAULT_CREDENTIALS_FILENAME,
97    authorized_user_filename=DEFAULT_AUTHORIZED_USER_FILENAME,
98):
99    r"""Authenticate with OAuth Client ID.
100
101    By default this function will use the local server strategy and open
102    the authorization URL in the user’s browser::
103
104        gc = gspread.oauth()
105
106    Another option is to run a console strategy. This way, the user is
107    instructed to open the authorization URL in their browser. Once the
108    authorization is complete, the user must then copy & paste the
109    authorization code into the application::
110
111        gc = gspread.oauth(flow=gspread.auth.console_flow)
112
113
114    ``scopes`` parameter defaults to read/write scope available in
115    ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets
116    and Drive API::
117
118        DEFAULT_SCOPES =[
119            'https://www.googleapis.com/auth/spreadsheets',
120            'https://www.googleapis.com/auth/drive'
121        ]
122
123    You can also use ``gspread.auth.READONLY_SCOPES`` for read only access.
124    Obviously any method of ``gspread`` that updates a spreadsheet
125    **will not work** in this case::
126
127        gc = gspread.oauth(scopes=gspread.auth.READONLY_SCOPES)
128
129        sh = gc.open("A spreadsheet")
130        sh.sheet1.update('A1', '42')   # <-- this will not work
131
132    If you're storing your user credentials in a place other than the
133    default, you may provide a path to that file like so::
134
135        gc = gspread.oauth(
136            credentials_filename='/alternative/path/credentials.json',
137            authorized_user_filename='/alternative/path/authorized_user.json',
138        )
139
140    :param list scopes: The scopes used to obtain authorization.
141    :param function flow: OAuth flow to use for authentication.
142        Defaults to :meth:`~gspread.auth.local_server_flow`
143    :param str credentials_filename: Filepath (including name) pointing to a
144        credentials `.json` file.
145        Defaults to DEFAULT_CREDENTIALS_FILENAME:
146            * `%APPDATA%\gspread\credentials.json` on Windows
147            * `~/.config/gspread/credentials.json` everywhere else
148    :param str authorized_user_filename: Filepath (including name) pointing to
149        an authorized user `.json` file.
150        Defaults to DEFAULT_AUTHORIZED_USER_FILENAME:
151            * `%APPDATA%\gspread\authorized_user.json` on Windows
152            * `~/.config/gspread/authorized_user.json` everywhere else
153
154    :rtype: :class:`gspread.Client`
155    """
156    creds = load_credentials(filename=authorized_user_filename)
157
158    if not creds:
159        with open(credentials_filename) as json_file:
160            client_config = json.load(json_file)
161        creds = flow(client_config=client_config, scopes=scopes)
162        store_credentials(creds, filename=authorized_user_filename)
163
164    client = Client(auth=creds)
165    return client
166
167
168def oauth_from_dict(
169    credentials=None,
170    authorized_user_info=None,
171    scopes=DEFAULT_SCOPES,
172    flow=local_server_flow,
173):
174    r"""Authenticate with OAuth Client ID.
175
176    By default this function will use the local server strategy and open
177    the authorization URL in the user's browser::
178
179        gc = gspread.oauth()
180
181    Another option is to run a console strategy. This way, the user is
182    instructed to open the authorization URL in their browser. Once the
183    authorization is complete, the user must then copy & paste the
184    authorization code into the application::
185
186        gc = gspread.oauth(flow=gspread.auth.console_flow)
187
188
189    ``scopes`` parameter defaults to read/write scope available in
190    ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets
191    and Drive API::
192
193        DEFAULT_SCOPES =[
194            'https://www.googleapis.com/auth/spreadsheets',
195            'https://www.googleapis.com/auth/drive'
196        ]
197
198    You can also use ``gspread.auth.READONLY_SCOPES`` for read only access.
199    Obviously any method of ``gspread`` that updates a spreadsheet
200    **will not work** in this case::
201
202        gc = gspread.oauth(scopes=gspread.auth.READONLY_SCOPES)
203
204        sh = gc.open("A spreadsheet")
205        sh.sheet1.update('A1', '42')   # <-- this will not work
206
207    This function requires you to pass the credentials directly as
208    a python dict. After the first authentication the function returns
209    the authenticated user info, this can be passed again to authenticate
210    the user without the need to run the flow again.
211
212        gc = gspread.oauth(
213            credentials=my_creds,
214            authorized_user_info=my_auth_user,
215        )
216
217    :param dict credentials: The credentials from google cloud platform
218    :param dict authorized_user_info: The authenticated user
219        if already authenticated.
220    :param list scopes: The scopes used to obtain authorization.
221    :param function flow: OAuth flow to use for authentication.
222        Defaults to :meth:`~gspread.auth.local_server_flow`
223
224    :rtype: :class:`gspread.Client`
225    """
226
227    creds = None
228    if authorized_user_info is not None:
229        creds = Credentials.from_authorized_user_info(authorized_user_info, scopes)
230
231    if not creds:
232        creds = flow(client_config=credentials, scopes=scopes)
233
234    client = Client(auth=creds)
235
236    # must return the creds to the user
237    # must strip the token an use the dedicated method from Credentials
238    # to return a dict "safe to store".
239    return (client, creds.to_json("token"))
240
241
242def service_account(filename=DEFAULT_SERVICE_ACCOUNT_FILENAME, scopes=DEFAULT_SCOPES):
243    """Authenticate using a service account.
244
245    ``scopes`` parameter defaults to read/write scope available in
246    ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets
247    and Drive API::
248
249        DEFAULT_SCOPES =[
250            'https://www.googleapis.com/auth/spreadsheets',
251            'https://www.googleapis.com/auth/drive'
252        ]
253
254    You can also use ``gspread.auth.READONLY_SCOPES`` for read only access.
255    Obviously any method of ``gspread`` that updates a spreadsheet
256    **will not work** in this case.
257
258    :param str filename: The path to the service account json file.
259    :param list scopes: The scopes used to obtain authorization.
260
261    :rtype: :class:`gspread.Client`
262    """
263    creds = ServiceAccountCredentials.from_service_account_file(filename, scopes=scopes)
264    return Client(auth=creds)
265
266
267def service_account_from_dict(info, scopes=DEFAULT_SCOPES):
268    """Authenticate using a service account (json).
269
270    ``scopes`` parameter defaults to read/write scope available in
271    ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets
272    and Drive API::
273
274        DEFAULT_SCOPES =[
275            'https://www.googleapis.com/auth/spreadsheets',
276            'https://www.googleapis.com/auth/drive'
277        ]
278
279    You can also use ``gspread.auth.READONLY_SCOPES`` for read only access.
280    Obviously any method of ``gspread`` that updates a spreadsheet
281    **will not work** in this case.
282
283    :param info (Mapping[str, str]): The service account info in Google format
284    :param list scopes: The scopes used to obtain authorization.
285
286    :rtype: :class:`gspread.Client`
287    """
288    creds = ServiceAccountCredentials.from_service_account_info(
289        info=info,
290        scopes=scopes,
291    )
292    return Client(auth=creds)
293