Skip to content

Commit b9dc4ae

Browse files
authored
Merge pull request #809 from planetlabs/auth-808
Clarify auth source, CLI only sources from config file, add planet auth store, Auth.write() -> Auth.store()
2 parents 4b57b60 + f10150d commit b9dc4ae

File tree

12 files changed

+179
-44
lines changed

12 files changed

+179
-44
lines changed

docs/get-started/quick-start-guide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ And you can see that the value was stored successfully as an environment variabl
8383
echo $PL_API_KEY
8484
```
8585

86-
!!!note "The API Key environment variable is always used first in the Planet SDK"
87-
If you do create a `PL_API_KEY` environment variable, the SDK will use this value. `PL_API_KEY` overrides the value that was retrieved using your Planet login with a call to `planet auth init`. The `planet auth value` call currently does not reflect that `PL_API_KEY` overrides the `auth init` value (this should be fixed in 2.0-beta.1 with [issue 643](https://github.com/planetlabs/planet-client-python/issues/643))
86+
!!!note "The API Key environment variable is ignored by the CLI but used by the Python library"
87+
If you do create a `PL_API_KEY` environment variable, the CLI will be unaffected but the Planet library will use this as the source for authorization instead of the value stored in `planet auth init`.
8888

8989
## Step 5: Search for Planet Imagery
9090

docs/python/sdk-guide.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,18 @@ asyncio.run(main())
4646

4747
## Authenticate with Planet services
4848

49-
There are two steps to managing authentication information, obtaining the
50-
authentication information from Planet and then managing it for local retrieval
51-
for authentication purposes.
49+
A `Session` requires authentication to communicate with Planet services. This
50+
authentication information is retrieved when a `Session` is created. By default,
51+
a `Session` obtains authorization from the following sources with order
52+
indicating priority:
5253

53-
The recommended method for obtaining authentication information is through
54-
logging in, using `Auth.from_login()` (note: using something like the `getpass`
55-
module is recommended to ensure your password remains secure). Alternatively,
56-
the api key can be obtained directly from the Planet account site and then used
57-
in `Auth.from_key()`.
54+
1. The environment variable `PL_API_KEY`
55+
2. The secret file
56+
57+
The SDK provides the `auth.Auth` class for managing authentication information.
58+
This module can be used to obtain authentication information from username
59+
and password with `Auth.from_login()`. Additionally, it can be created with
60+
the API key obtained directly from the Planet account site with `Auth.from_key(<API_KEY>)`.
5861

5962
Once the authentication information is obtained, the most convenient way of
6063
managing it for local use is to write it to a secret file using `Auth.write()`.
@@ -69,16 +72,15 @@ from planet import Auth
6972

7073
pw = getpass.getpass()
7174
auth = Auth.from_login('user', 'pw')
72-
auth.write()
75+
auth.store()
7376

7477
```
7578

76-
When a `Session` is created, by default, authentication is read from the secret
77-
file created with `Auth.write()`. This behavior can be modified by specifying
79+
The default authentication behavior of the `Session` can be modified by specifying
7880
`Auth` explicitly using the methods `Auth.from_file()` and `Auth.from_env()`.
7981
While `Auth.from_key()` and `Auth.from_login` can be used, it is recommended
8082
that those functions be used in authentication initialization and the
81-
authentication information be stored using `Auth.write()`.
83+
authentication information be stored using `Auth.store()`.
8284

8385
The file and environment variable read from can be customized in the
8486
respective functions. For example, authentication can be read from a custom

planet/auth.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@
2525
import jwt
2626

2727
from . import http
28-
from .constants import PLANET_BASE_URL, SECRET_FILE_PATH
28+
from .constants import ENV_API_KEY, PLANET_BASE_URL, SECRET_FILE_PATH
2929
from .exceptions import AuthException
3030
from typing import Optional
3131

3232
LOGGER = logging.getLogger(__name__)
3333

3434
BASE_URL = f'{PLANET_BASE_URL}/v0/auth'
35-
ENV_API_KEY = 'PL_API_KEY'
3635

3736
AuthType = httpx.Auth
3837

@@ -135,9 +134,9 @@ def value(self):
135134
def to_dict(self) -> dict:
136135
pass
137136

138-
def write(self,
137+
def store(self,
139138
filename: Optional[typing.Union[str, pathlib.Path]] = None):
140-
'''Write authentication information.
139+
'''Store authentication information in secret file.
141140
142141
Parameters:
143142
filename: Alternate path for the planet secret file.

planet/cli/auth.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
# limitations under the License.
1414
"""Auth API CLI"""
1515
import logging
16+
import os
1617

1718
import click
1819

1920
import planet
21+
from planet.constants import ENV_API_KEY
2022
from .cmds import translate_exceptions
2123

2224
LOGGER = logging.getLogger(__name__)
@@ -48,12 +50,32 @@ def init(ctx, email, password):
4850
'''Obtain and store authentication information'''
4951
base_url = ctx.obj['BASE_URL']
5052
plauth = planet.Auth.from_login(email, password, base_url=base_url)
51-
plauth.write()
53+
plauth.store()
5254
click.echo('Initialized')
55+
if os.getenv(ENV_API_KEY):
56+
click.echo(f'Warning - Environment variable {ENV_API_KEY} already '
57+
'exists. To update, with the new value, use the following:')
58+
click.echo(f'export {ENV_API_KEY}=$(planet auth value)')
5359

5460

5561
@auth.command()
5662
@translate_exceptions
5763
def value():
5864
'''Print the stored authentication information'''
5965
click.echo(planet.Auth.from_file().value)
66+
67+
68+
@auth.command()
69+
@translate_exceptions
70+
@click.argument('key')
71+
def store(key):
72+
'''Store authentication information'''
73+
plauth = planet.Auth.from_key(key)
74+
if click.confirm('This overrides the stored value. Continue?'):
75+
plauth.store()
76+
click.echo('Updated')
77+
if os.getenv(ENV_API_KEY):
78+
click.echo(f'Warning - Environment variable {ENV_API_KEY} already '
79+
'exists. To update, with the new value, use the '
80+
'following:')
81+
click.echo(f'export {ENV_API_KEY}=$(planet auth value)')

planet/cli/data.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@
3737

3838
@asynccontextmanager
3939
async def data_client(ctx):
40-
auth = ctx.obj['AUTH']
41-
base_url = ctx.obj['BASE_URL']
42-
async with CliSession(auth=auth) as sess:
43-
cl = DataClient(sess, base_url=base_url)
40+
async with CliSession() as sess:
41+
cl = DataClient(sess, base_url=ctx.obj['BASE_URL'])
4442
yield cl
4543

4644

@@ -52,7 +50,6 @@ async def data_client(ctx):
5250
help='Assign custom base Orders API URL.')
5351
def data(ctx, base_url):
5452
'''Commands for interacting with the Data API'''
55-
ctx.obj['AUTH'] = None
5653
ctx.obj['BASE_URL'] = base_url
5754

5855

planet/cli/orders.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131

3232
@asynccontextmanager
3333
async def orders_client(ctx):
34-
auth = ctx.obj['AUTH']
3534
base_url = ctx.obj['BASE_URL']
36-
async with CliSession(auth=auth) as sess:
35+
async with CliSession() as sess:
3736
cl = OrdersClient(sess, base_url=base_url)
3837
yield cl
3938

@@ -46,7 +45,6 @@ async def orders_client(ctx):
4645
help='Assign custom base Orders API URL.')
4746
def orders(ctx, base_url):
4847
'''Commands for interacting with the Orders API'''
49-
ctx.obj['AUTH'] = None
5048
ctx.obj['BASE_URL'] = base_url
5149

5250

planet/cli/session.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""CLI HTTP/auth sessions."""
22

3-
from planet.auth import AuthType
3+
from planet.auth import Auth
44
from planet.http import Session
5-
from typing import Optional
65

76

87
class CliSession(Session):
8+
"""Session with CLI-specific auth and identifying header"""
99

10-
def __init__(self, auth: Optional[AuthType] = None):
11-
super().__init__(auth)
10+
def __init__(self):
11+
super().__init__(Auth.from_file())
1212
self._client.headers.update({'X-Planet-App': 'python-cli'})

planet/cli/subscriptions.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
@click.pass_context
1515
def subscriptions(ctx):
1616
'''Commands for interacting with the Subscriptions API'''
17-
# None means that order of precedence is 1) environment variable,
18-
# 2) secret file.
19-
ctx.obj['AUTH'] = None
17+
pass
2018

2119

2220
# We want our command to be known as "list" on the command line but
@@ -40,7 +38,7 @@ def subscriptions(ctx):
4038
@coro
4139
async def list_subscriptions_cmd(ctx, status, limit, pretty):
4240
"""Prints a sequence of JSON-encoded Subscription descriptions."""
43-
async with CliSession(auth=ctx.obj['AUTH']) as session:
41+
async with CliSession() as session:
4442
client = SubscriptionsClient(session)
4543
async for sub in client.list_subscriptions(status=status, limit=limit):
4644
echo_json(sub, pretty)
@@ -75,7 +73,7 @@ def parse_request(ctx, param, value: str) -> dict:
7573
@coro
7674
async def create_subscription_cmd(ctx, request, pretty):
7775
"""Submits a subscription request and prints the API response."""
78-
async with CliSession(auth=ctx.obj['AUTH']) as session:
76+
async with CliSession() as session:
7977
client = SubscriptionsClient(session)
8078
sub = await client.create_subscription(request)
8179
echo_json(sub, pretty)
@@ -89,7 +87,7 @@ async def create_subscription_cmd(ctx, request, pretty):
8987
@coro
9088
async def cancel_subscription_cmd(ctx, subscription_id, pretty):
9189
"""Cancels a subscription and prints the API response."""
92-
async with CliSession(auth=ctx.obj['AUTH']) as session:
90+
async with CliSession() as session:
9391
client = SubscriptionsClient(session)
9492
sub = await client.cancel_subscription(subscription_id)
9593
echo_json(sub, pretty)
@@ -104,7 +102,7 @@ async def cancel_subscription_cmd(ctx, subscription_id, pretty):
104102
@coro
105103
async def update_subscription_cmd(ctx, subscription_id, request, pretty):
106104
"""Updates a subscription and prints the API response."""
107-
async with CliSession(auth=ctx.obj['AUTH']) as session:
105+
async with CliSession() as session:
108106
client = SubscriptionsClient(session)
109107
sub = await client.update_subscription(subscription_id, request)
110108
echo_json(sub, pretty)
@@ -118,7 +116,7 @@ async def update_subscription_cmd(ctx, subscription_id, request, pretty):
118116
@coro
119117
async def describe_subscription_cmd(ctx, subscription_id, pretty):
120118
"""Gets the description of a subscription and prints the API response."""
121-
async with CliSession(auth=ctx.obj['AUTH']) as session:
119+
async with CliSession() as session:
122120
client = SubscriptionsClient(session)
123121
sub = await client.get_subscription(subscription_id)
124122
echo_json(sub, pretty)
@@ -154,7 +152,7 @@ async def list_subscription_results_cmd(ctx,
154152
status,
155153
limit):
156154
"""Gets results of a subscription and prints the API response."""
157-
async with CliSession(auth=ctx.obj['AUTH']) as session:
155+
async with CliSession() as session:
158156
client = SubscriptionsClient(session)
159157
async for result in client.get_results(subscription_id,
160158
status=status,

planet/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
DATA_DIR = Path(os.path.dirname(__file__)) / 'data'
2222

23+
ENV_API_KEY = 'PL_API_KEY'
24+
2325
PLANET_BASE_URL = 'https://api.planet.com'
2426

2527
SECRET_FILE_PATH = Path(os.path.expanduser('~')) / '.planet.json'

tests/integration/test_auth_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# the License.
1414
from http import HTTPStatus
1515
import json
16+
import os
1617

1718
from click.testing import CliRunner
1819
import httpx
@@ -104,3 +105,19 @@ def test_cli_auth_value_failure(redirect_secretfile):
104105
assert result.exception
105106
assert 'Error: Auth information does not exist or is corrupted.' \
106107
in result.output
108+
109+
110+
def test_cli_auth_store_cancel(redirect_secretfile):
111+
result = CliRunner().invoke(cli.main, ['auth', 'store', 'setval'],
112+
input='')
113+
assert not result.exception
114+
assert not os.path.isfile(redirect_secretfile)
115+
116+
117+
def test_cli_auth_store_confirm(redirect_secretfile):
118+
result = CliRunner().invoke(cli.main, ['auth', 'store', 'setval'],
119+
input='y')
120+
assert not result.exception
121+
122+
with open(redirect_secretfile, 'r') as f:
123+
assert json.load(f) == {'key': 'setval'}

0 commit comments

Comments
 (0)