-
Notifications
You must be signed in to change notification settings - Fork 99
Expand file tree
/
Copy pathauth.py
More file actions
568 lines (484 loc) · 24.5 KB
/
Copy pathauth.py
File metadata and controls
568 lines (484 loc) · 24.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# Copyright 2020 Planet Labs, Inc.
# Copyright 2022, 2024, 2025 Planet Labs PBC.
#
# 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.
"""Manage authentication with Planet APIs"""
from __future__ import annotations # https://stackoverflow.com/a/33533514
import abc
import copy
import os
import pathlib
import typing
import warnings
import httpx
from .auth_builtins import _ProductionEnv, _OIDC_AUTH_CLIENT_CONFIG__USER_SKEL, _OIDC_AUTH_CLIENT_CONFIG__M2M_SKEL
import planet_auth
import planet_auth_utils
from .constants import SECRET_FILE_PATH
from .exceptions import PlanetError
planet_auth.setStructuredLogging(nested_key=None)
class Auth(abc.ABC, httpx.Auth):
"""
Handle authentication information for use with Planet APIs.
Static constructor methods should be used to create an auth context
that can be used by Planet API client modules to authenticate
requests made to the Planet service.
"""
@staticmethod
def _normalize_profile_name(profile_name: str):
if profile_name.find(os.sep) != -1:
raise ValueError(f"Profile names cannot contain '{os.sep}'")
return profile_name.lower()
@staticmethod
def from_user_default_session() -> Auth:
"""
Create authentication context from user defaults.
This method should be used when an application wants to defer
auth profile management to the user and the `planet auth` CLI
command entirely.
Users may use the `planet auth login` and `planet auth profile
commands to initialize and manage sessions.
Defaults take into account environment variables (highest priority),
user configuration saved to `~/.planet.json` and `~/.planet/`
(next priority), and built-in defaults (lowest priority).
This method does not support the use a custom storage provider.
Environment Variables:
| Variable Name | Description |
| --------------------- | ------------------------------------------------------------------ |
| PL_AUTH_CLIENT_ID | Specify an OAuth2 M2M client ID |
| PL_AUTH_CLIENT_SECRET | Specify an OAuth2 M2M client secret |
| PL_AUTH_API_KEY | Specify a legacy Planet API key |
| PL_AUTH_PROFILE | Specify a previously saved planet_auth library auth client profile |
"""
return _PLAuthLibAuth(plauth=planet_auth_utils.PlanetAuthFactory.
initialize_auth_client_context())
@staticmethod
def from_profile(
profile_name: str,
save_state_to_storage: bool = True,
) -> Auth:
"""
Create authentication context from an auth session that has been
initialized and saved to `~/.planet.json` and `~/.planet/`.
Users can initialize and save such a session out-of-band
using the `planet auth login` and `planet auth profile` commands.
To initialize this session programmatically without the CLI,
you must complete an OAuth2 user login flow with one of the login
methods on this class. The login method used must be compatible
with the specified profile.
This method does not support the use a custom storage provider.
In addition to sharing sessions with other programs through the user's
home directory, this method may also be used to load SDK built-in
client profiles. This is provided as a developer convenience.
Applications _should_ register unique client IDs with the Planet service
and use `from_oauth_user_auth_code()` or `from_oauth_user_device_code()`
to create profiles unique to the application.
At present, the following built-in profiles are available:
| Profile Name | Description |
| ------------ | -------------------------------------------------------------------- |
| `planet-user` | User interactive OAuth2 client profile shared with the `planet` CLI. |
Parameters:
profile_name: Named profile from which to load auth configuration
and state. This should be a name of a CLI managed profile.
save_state_to_storage: Boolean controlling whether login sessions
should be saved to storage. This nearly always should be true,
since this constructor exists to share state through storage
backed profiles. The only exception may be when using a SDK
built-in profile in an application that should not attempt to
save state to disk.
"""
if not profile_name:
raise APIKeyAuthException('Profile name cannot be empty.')
pl_authlib_context = planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context(
auth_profile_opt=profile_name,
save_token_file=save_state_to_storage,
save_profile_config=save_state_to_storage)
return _PLAuthLibAuth(plauth=pl_authlib_context)
# TODO: add support for confidential clients
@staticmethod
def from_oauth_user_auth_code(
client_id: str,
callback_url: str,
requested_scopes: typing.Optional[typing.List[str]] = None,
save_state_to_storage: bool = True,
profile_name: typing.Optional[str] = None,
storage_provider: typing.Optional[
planet_auth.ObjectStorageProvider] = None,
) -> Auth:
"""
Create authentication context for the specified registered client
application.
Developers of applications must register clients with
Planet, and will be issued a Client ID as part of that process.
Developers should register a client for each distinct application so
that end-users may discretely manage applications permitted to access
Planet APIs on their behalf.
This method does not perform a user login to initialize a session.
If not initialized out of band using the CLI, sessions must be initialized
with the user_login() before API calls may be made.
Parameters:
client_id: Client ID
requested_scopes: List of requested OAuth2 scopes
callback_url: Client callback URL
profile_name: User friendly name to use when saving the configuration
to storage per the `save_state_to_storage` flag. The profile name
will be normalized to a file system compatible identifier,
regardless of storage provider.
save_state_to_storage: Boolean controlling whether login sessions
should be saved to storage. When the default storage provider is
used, they will be stored in a way that is compatible with
the `planet` CLI.
storage_provider: A custom storage provider to save session state
for the application.
"""
plauth_config_dict = copy.deepcopy(_OIDC_AUTH_CLIENT_CONFIG__USER_SKEL)
plauth_config_dict["client_type"] = "oidc_auth_code"
plauth_config_dict["client_id"] = client_id
if requested_scopes:
plauth_config_dict["scopes"] = requested_scopes
plauth_config_dict["redirect_uri"] = callback_url
if not profile_name:
profile_name = client_id
normalized_profile_name = Auth._normalize_profile_name(profile_name)
pl_authlib_context = planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context_from_custom_config(
client_config=plauth_config_dict,
initial_token_data={},
save_token_file=save_state_to_storage,
profile_name=normalized_profile_name,
save_profile_config=save_state_to_storage,
storage_provider=storage_provider,
)
return Auth._from_plauth(pl_authlib_context)
# TODO: add support for confidential clients
@staticmethod
def from_oauth_user_device_code(
client_id: str,
requested_scopes: typing.Optional[typing.List[str]] = None,
save_state_to_storage: bool = True,
profile_name: typing.Optional[str] = None,
storage_provider: typing.Optional[
planet_auth.ObjectStorageProvider] = None
) -> Auth:
"""
Create authentication context for the specified registered client
application.
Developers of applications must register clients with
Planet, and will be issued a Client ID as part of that process.
Developers should register a client for each distinct application so
that end-users may discretely manage applications permitted to access
Planet APIs on their behalf.
This method does not perform a user login to initialize a session.
This method does not perform a user login to initialize a session.
If not initialized out of band using the CLI, sessions must be initialized
with the device login methods `device_user_login_initiate()` and
`device_user_login_complete()` before API calls may be made.
Parameters:
client_id: Client ID
requested_scopes: List of requested OAuth2 scopes
profile_name: User friendly name to use when saving the configuration
to storage per the `save_state_to_storage` flag. The profile name
will be normalized to file system compatible identifier, regardless
of the storage provider being used.
save_state_to_storage: Boolean controlling whether login sessions
should be saved to storage. When the default storage provider is
used, they will be stored in a way that is compatible with
the `planet` CLI.
storage_provider: A custom storage provider to save session state
for the application.
"""
plauth_config_dict = copy.deepcopy(_OIDC_AUTH_CLIENT_CONFIG__USER_SKEL)
plauth_config_dict["client_type"] = "oidc_device_code"
plauth_config_dict["client_id"] = client_id
if requested_scopes:
plauth_config_dict["scopes"] = requested_scopes
if not profile_name:
profile_name = client_id
normalized_profile_name = Auth._normalize_profile_name(profile_name)
pl_authlib_context = planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context_from_custom_config(
client_config=plauth_config_dict,
initial_token_data={},
save_token_file=save_state_to_storage,
profile_name=normalized_profile_name,
save_profile_config=save_state_to_storage,
storage_provider=storage_provider,
)
return Auth._from_plauth(pl_authlib_context)
@staticmethod
def from_oauth_m2m(
client_id: str,
client_secret: str,
requested_scopes: typing.Optional[typing.List[str]] = None,
save_state_to_storage: bool = True,
profile_name: typing.Optional[str] = None,
storage_provider: typing.Optional[
planet_auth.ObjectStorageProvider] = None,
) -> Auth:
"""
Create authentication from the specified OAuth2 service account
client ID and secret.
Parameters:
client_id: Planet service account client ID.
client_secret: Planet service account client secret.
requested_scopes: List of requested OAuth2 scopes
profile_name: User friendly name to use when saving the configuration
to storage per the `save_state_to_storage` flag. The profile name
will be normalized to a file system compatible identifier regardless
of the storage provider being used.
save_state_to_storage: Boolean controlling whether login sessions
should be saved to storage. When the default storage provider is
used, they will be stored in a way that is compatible with
the `planet` CLI.
storage_provider: A custom storage provider to save session state
for the application.
"""
plauth_config_dict = copy.deepcopy(_OIDC_AUTH_CLIENT_CONFIG__M2M_SKEL)
plauth_config_dict["client_id"] = client_id
plauth_config_dict["client_secret"] = client_secret
if requested_scopes:
plauth_config_dict["scopes"] = requested_scopes
if not profile_name:
profile_name = client_id
normalized_profile_name = Auth._normalize_profile_name(profile_name)
pl_authlib_context = planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context_from_custom_config(
client_config=plauth_config_dict,
initial_token_data={},
save_token_file=save_state_to_storage,
profile_name=normalized_profile_name,
save_profile_config=save_state_to_storage,
storage_provider=storage_provider,
)
return Auth._from_plauth(pl_authlib_context)
@staticmethod
def _from_plauth(pl_authlib_context: planet_auth.Auth) -> Auth:
"""
Create authentication from the provided Planet Auth Library
Authentication Context. Generally, applications will want to use one
of the Auth Library factory helpers to construct this context (See the
factory class).
This method is intended for advanced use cases where the developer
has their own client ID registered, and is familiar with the
Planet Auth Library. (Registering client IDs is a feature of the
Planet Platform not yet released to the public as of January 2025.)
"""
return _PLAuthLibAuth(plauth=pl_authlib_context)
@staticmethod
def from_key(key: typing.Optional[str]) -> Auth:
"""Obtain authentication from api key.
Parameters:
key: Planet API key
"""
if not key:
raise APIKeyAuthException('API key cannot be empty.')
pl_authlib_context = planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context(
auth_api_key_opt=key,
save_token_file=False,
)
return _PLAuthLibAuth(plauth=pl_authlib_context)
@staticmethod
def from_file(
filename: typing.Optional[typing.Union[str,
pathlib.Path]] = None) -> Auth:
"""Create authentication from secret file.
The default secret file is named `.planet.json` and is stored in the user
directory. The file has a special format and should have been created
with `Auth.write()`.
Pending deprecation:
OAuth2, which should replace API keys in most cases does not have
a direct replacement for "from_file()" in many cases.
The format of the `.planet.json file` is changing with the
migration of Planet APIs to OAuth2. With that, this method is
also being deprecated as a means to bootstrap auth configuration
with a simple API key. For the time being this method will still
be supported, but this method will fail if the file is present
with only new configuration fields, and lacks the legacy API key
field.
Parameters:
filename: Alternate path for the planet secret file.
"""
warnings.warn("Auth.from_file() will be deprecated.",
PendingDeprecationWarning)
plauth_config = {
**_ProductionEnv.LEGACY_AUTH_AUTHORITY,
"client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get(
"client_type"),
}
pl_authlib_context = planet_auth.Auth.initialize_from_config_dict(
client_config=plauth_config,
token_file=filename or SECRET_FILE_PATH)
return _PLAuthLibAuth(plauth=pl_authlib_context)
@staticmethod
def from_env(variable_name: typing.Optional[str] = None) -> Auth:
"""Create authentication from environment variables.
Reads the `PL_API_KEY` environment variable
Pending Deprecation:
This method is pending deprecation. The method `from_user_default_session()`
considers environment variables and configuration files through
the planet_auth and planet_auth_utils libraries, and works with
legacy API keys, OAuth2 M2M clients, and OAuth2 interactive profiles.
This method should be used in most cases as a replacement.
Parameters:
variable_name: Alternate environment variable.
"""
warnings.warn(
"from_env() will be deprecated. Use from_user_default_session() in most"
" cases, which will consider both environment variables and user"
" configuration files.",
PendingDeprecationWarning)
variable_name = variable_name or planet_auth_utils.EnvironmentVariables.AUTH_API_KEY
api_key = os.getenv(variable_name, None)
return Auth.from_key(api_key)
@staticmethod
def from_login(email: str,
password: str,
base_url: typing.Optional[str] = None) -> Auth:
raise DeprecationWarning(
"Auth.from_login() has been deprecated. Use Auth.from_user_session()."
)
@classmethod
def from_dict(cls, data: dict) -> Auth:
raise DeprecationWarning("Auth.from_dict() has been deprecated.")
def to_dict(self) -> dict:
raise DeprecationWarning("Auth.to_dict() has been deprecated.")
def store(self,
filename: typing.Optional[typing.Union[str,
pathlib.Path]] = None):
raise DeprecationWarning("Auth.store() has been deprecated.")
@property
def value(self):
raise DeprecationWarning("Auth.value has been deprecated.")
@abc.abstractmethod
def user_login(
self,
allow_open_browser: typing.Optional[bool] = False,
allow_tty_prompt: typing.Optional[bool] = False,
):
"""
Perform an interactive login. User interaction will be via the TTY
and/or a local web browser, with the details dependent on the
client auth configuration.
:param allow_open_browser:
:param allow_tty_prompt:
"""
@abc.abstractmethod
def device_user_login_initiate(self) -> dict:
"""
Initiate a user login that uses the OAuth2 Device Code Flow for applications
that cannot operate a browser locally. The returned dictionary should be used
to prompt the user to complete the process, and will conform to RFC 8628.
"""
@abc.abstractmethod
def device_user_login_complete(self, login_initialization_info: dict):
"""
Complete a user login that uses the OAuth2 Device Code Flow for applications
that was initiated by a call to `device_user_login_initiate()`. The structure
that was returned from `device_user_login_initiate()` should be passed
to this function unaltered after it has been used to prompt the user.
"""
@abc.abstractmethod
def is_initialized(self) -> bool:
"""
Check whether the user session has been initialized. For OAuth2
user based sessions, this means that a login has been performed
or saved login session data has been located. For M2M and API Key
sessions, this should be true if keys or secrets have been
properly configured. The network will not be probed, and the user
will not be prompted by this method.
Expired sessions or invalid credentials will not be detected.
See `ensure_initialized()` for a method that will check the validity
of sessions.
"""
@abc.abstractmethod
def ensure_initialized(
self,
allow_open_browser: typing.Optional[bool] = False,
allow_tty_prompt: typing.Optional[bool] = False,
) -> None:
"""
Do everything necessary to ensure the auth context is ready for use,
while still biasing towards just-in-time operations and not making
unnecessary network requests or prompts for user interaction.
This can be more complex than it sounds given the variations in
possible session state. Clients may be initialized with active
sessions, initialized with stale but still valid sessions,
initialized with invalid or expired sessions, or completely
uninitialized. The process taken to ensure client readiness with
as little user disruption as possible is as follows:
1. If the client has been logged in and has a non-expired
session, the client will be considered ready without prompting
the user or probing the network. This will not require user
interaction.
2. If the client has not been logged in and is an M2M client,
the client will be considered ready without prompting
the user or probing the network. This will not require
user interaction. Login will be delayed until it is required.
3. If the client has been logged in and has an expired access token,
the network will be probed to attempt a refresh of the session.
This should not require user interaction. If refresh fails,
the user will be prompted to perform a fresh login, requiring
user interaction.
4. If the client has never been logged in and is a user interactive
client (verses an M2M client), a user interactive login will be
initiated.
There still may be conditions where we believe we are
ready, but requests will still ultimately fail. Saved secrets for M2M
clients could be wrong, or the user could be denied by API access
rules that are independent of session authentication.
When a user interactive login is required, the client must specify
whether a local web browser may be opened and/or whether the TTY
may be used to prompt the user. What is appropriate will depend
on the nature of the application using the Planet SDK.
If the auth context cannot be made ready, an exception will be raised.
Parameters:
allow_open_browser: specify whether login is permitted to open
a local browser window.
allow_tty_prompt: specify whether login is permitted to request
input from the terminal.
"""
class APIKeyAuthException(PlanetError):
"""exceptions thrown by APIKeyAuth"""
pass
class _PLAuthLibAuth(Auth):
# The Planet Auth Library uses a "has a" authenticator pattern for its
# planet_auth.Auth context class. This SDK library employs a "is a"
# authenticator design pattern for users of its Auth context obtained
# from the constructors above. This class smooths over that design
# difference as we move to using the Planet Auth Library.
def __init__(self, plauth: planet_auth.Auth):
self._plauth = plauth
def auth_flow(self, r: httpx._models.Request):
return self._plauth.request_authenticator().auth_flow(r)
def user_login(
self,
allow_open_browser: typing.Optional[bool] = False,
allow_tty_prompt: typing.Optional[bool] = False,
):
self._plauth.login(
allow_open_browser=allow_open_browser,
allow_tty_prompt=allow_tty_prompt,
)
def device_user_login_initiate(self) -> dict:
return self._plauth.device_login_initiate()
def device_user_login_complete(self, login_initialization_info: dict):
return self._plauth.device_login_complete(login_initialization_info)
def is_initialized(self) -> bool:
return self._plauth.request_authenticator_is_ready()
def ensure_initialized(
self,
allow_open_browser: typing.Optional[bool] = False,
allow_tty_prompt: typing.Optional[bool] = False,
) -> None:
return self._plauth.ensure_request_authenticator_is_ready(
allow_open_browser=allow_open_browser,
allow_tty_prompt=allow_tty_prompt,
)
AuthType = Auth