Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new CLI auth flow w/ tests and docs #1431

Merged
merged 12 commits into from
Aug 30, 2019
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ These changes are available in the [master branch](https://github.com/PrefectHQ/
- Support persistent `scheduled_start_time` for scheduled flow runs when run locally with `flow.run()` - [#1418](https://github.com/PrefectHQ/prefect/pull/1418), [#1429](https://github.com/PrefectHQ/prefect/pull/1429)
- Add `task_args` to `Task.map` - [#1390](https://github.com/PrefectHQ/prefect/issues/1390)
- Add auth flows for `USER`-scoped Cloud API tokens - [#1423](https://github.com/PrefectHQ/prefect/pull/1423)
- Add CLI commands for working with Prefect Cloud auth - [#1431](https://github.com/PrefectHQ/prefect/pull/1431)

### Task Library

Expand Down
87 changes: 87 additions & 0 deletions docs/cloud/cloud_concepts/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ TIMESTAMP LEVEL MESSAGE

`auth` is a group of commands that handle authentication related configuration with Prefect Cloud.

::: warning Config
Having an API token set as a config value prior to using auth CLI commands, either in your config.toml or as an environment variable, will cause all auth commands to abort on use.
:::

#### auth login

Running `prefect auth login` requires that a Prefect Cloud API token be provided and when executed the API token is used to login to Prefect Cloud.
Expand All @@ -373,6 +377,89 @@ $ prefect auth login --token BAD_TOKEN
Error attempting to use Prefect API token BAD_TOKEN
```

#### auth logout

Running `prefect auth logout` will log you out of your active tenant (if you are logged in)
```
$ prefect auth logout
Are you sure you want to log out of Prefect Cloud? (y/N) Y
Logged out from tenant PREVIOUS_ACTIVE_TENANT_ID
```

If there is no current active tenant then you should see:
```
$ prefect auth logout
Are you sure you want to log out of Prefect Cloud? (y/N) Y
No tenant currently active
```

#### auth list-tenants

Running `prefect auth list-tenants` will output all of the tenants that you have access to use.
```
$ prefect auth list-tenants
NAME SLUG ID
Test Person test-person 816sghf2-4d51-4338-a333-1771gns7614d
test@prefect.io's Account test-prefect-io-s-account 1971hs9f-e8ha-4a33-8c33-64512gds86g1 *
```

#### auth switch-tenants

Running `prefect auth switch-tenants --id TENANT_ID --slug TENANT_SLUG` will switch your active tenants. Either the tenant ID or the tenant slug needs to be provided.
```
$ prefect auth switch-tenants --slug test-person
Tenant switched
```

If you are unable to switch tenants for various reasons (bad id, bad slug, not providing either) then you should see:
```
$ prefect auth switch-tenants --slug test-person
Unable to switch tenant
```

#### auth create-token

Running `prefect auth create-token --name MY_TOKEN --role RUNNER` will generate a Prefect Cloud API token and output it to stdout. For more information on API tokens go [here](./api.html).
```
$ prefect auth create-token -n MyToken -r RUNNER
...token output...
```

If you are unable to create an API token then you should see:
```
$ prefect auth create-token -n MyToken -r RUNNER
Issue creating API token
```

#### auth revoke-token

Running `prefect auth revoke-token --id TOKEN_ID` will revoke API tokens in Prefect Cloud.
```
$ prefect auth revoke-token --id TOKEN_ID
Token successfully revoked
```

If the token is not found then you should see:
```
$ prefect auth revoke-token --id TOKEN_ID
Unable to revoke token with ID TOKEN_ID
```

#### auth list-tokens

Running `prefect auth list-tokens` will list your available API tokens in Prefect Cloud. Note: only the name and ID of the token will be shown, not the actual token.
```
$ prefect auth list-tokens
NAME ID
My_Token 87gh22f4-333c-47fc-ae8f-0b61ghu811c3
```

If you are unable to list API tokens then you should see:
```
$ prefect auth list-tokens
Unable to list API tokens
```

## Miscellaneous Commands

`prefect version` outputs the current version of Prefect you are using:
Expand Down
233 changes: 222 additions & 11 deletions src/prefect/cli/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import click
from click.exceptions import Abort
from tabulate import tabulate

from prefect import Client, config
from prefect.utilities.exceptions import AuthorizationError, ClientError


def check_override_auth_token():
if config.cloud.get("auth_token"):
click.secho("Auth token already present in config.", fg="red")
raise Abort


@click.group(hidden=True)
def auth():
"""
Expand All @@ -15,11 +23,34 @@ def auth():

\b
Arguments:
login Login to Prefect Cloud
login Log in to Prefect Cloud
logout Log out of Prefect Cloud
list-tenants List your available tenants
switch-tenants Switch to a different tenant
create-token Create an API token

\b
Examples:
$ prefect auth login --token MY_TOKEN
Log in successful

\b
$ prefect auth logout
Logged out from tenant TENANT_ID

\b
$ prefect auth list-tenants
NAME SLUG ID
Test Person test-person 816sghf2-4d51-4338-a333-1771gns7614d
test@prefect.io's Account test-prefect-io-s-account 1971hs9f-e8ha-4a33-8c33-64512gds86g1 *

\b
$ prefect auth switch-tenants --slug test-person
Tenant switched

\b
$ prefect auth create-token -n MyToken -r RUNNER
...token output...
"""
pass

Expand All @@ -30,25 +61,30 @@ def auth():
)
def login(token):
"""
Login to Prefect Cloud with an api token to use for Cloud communication.
Log in to Prefect Cloud with an api token to use for Cloud communication.

\b
Options:
--token, -t TEXT A Prefect Cloud api token [required]
"""

if config.cloud.get("auth_token"):
click.confirm(
"Prefect Cloud API token already set in config. Do you want to override?",
default=True,
abort=True,
)
check_override_auth_token()

client = Client(api_token=token)

# Verify login obtained a valid api token
try:
client.graphql(query={"query": {"tenant": "id"}})
output = client.graphql(
query={"query": {"user": {"default_membership": "tenant_id"}}}
)

# Log into default membership
success_login = client.login_to_tenant(
tenant_id=output.data.user[0].default_membership.tenant_id
)

if not success_login:
raise AuthorizationError

except AuthorizationError:
click.secho(
"Error attempting to use Prefect API token {}".format(token), fg="red"
Expand All @@ -61,4 +97,179 @@ def login(token):
# save token
client.save_api_token()

click.secho("Login successful", fg="green")
click.secho("Log in successful", fg="green")


@auth.command(hidden=True)
def logout():
"""
Log out of Prefect Cloud
"""
check_override_auth_token()

click.confirm(
"Are you sure you want to log out of Prefect Cloud?", default=False, abort=True
)

client = Client()
tenant_id = client._active_tenant_id

if not tenant_id:
click.secho("No tenant currently active", fg="red")
return

client.logout_from_tenant()

click.secho("Logged out from tenant {}".format(tenant_id), fg="green")


@auth.command(hidden=True)
def list_tenants():
"""
List available tenants
"""
check_override_auth_token()

client = Client()

tenants = client.get_available_tenants()
active_tenant_id = client._active_tenant_id

output = []
for item in tenants:
active = None
if item.id == active_tenant_id:
active = "*"
output.append([item.name, item.slug, item.id, active])

click.echo(
tabulate(
output,
headers=["NAME", "SLUG", "ID", ""],
tablefmt="plain",
numalign="left",
stralign="left",
)
)


@auth.command(hidden=True)
@click.option(
"--id", "-i", required=False, help="A Prefect Cloud tenant id.", hidden=True
)
@click.option(
"--slug", "-s", required=False, help="A Prefect Cloud tenant slug.", hidden=True
)
def switch_tenants(id, slug):
"""
Switch active tenant

\b
Options:
--id, -i TEXT A Prefect Cloud tenant id
--slug, -s TEXT A Prefect Cloud tenant slug
"""
check_override_auth_token()

client = Client()

login_success = client.login_to_tenant(tenant_slug=slug, tenant_id=id)
if not login_success:
click.secho("Unable to switch tenant", fg="red")
return

click.secho("Tenant switched", fg="green")


@auth.command(hidden=True)
@click.option("--name", "-n", required=True, help="A token name.", hidden=True)
@click.option("--role", "-r", required=True, help="A token role.", hidden=True)
def create_token(name, role):
"""
Create a Prefect Cloud API token.

For more info on API tokens visit https://docs.prefect.io/cloud/cloud_concepts/api.html

\b
Options:
--name, -n TEXT A name to give the generated token
--role, -r TEXT A role for the token
"""
check_override_auth_token()

client = Client()

output = client.graphql(
query={
"mutation($input: createAPITokenInput!)": {
"createAPIToken(input: $input)": {"token"}
}
},
variables=dict(input=dict(name=name, role=role)),
)

if not output.get("data", None):
click.secho("Issue creating API token", fg="red")
return

click.echo(output.data.createAPIToken.token)


@auth.command(hidden=True)
def list_tokens():
"""
List your available Prefect Cloud API tokens.
"""
check_override_auth_token()

client = Client()

output = client.graphql(query={"query": {"api_token": {"id", "name"}}})

if not output.get("data", None):
click.secho("Unable to list API tokens", fg="red")
return

tokens = []
for item in output.data.api_token:
tokens.append([item.name, item.id])

click.echo(
tabulate(
tokens,
headers=["NAME", "ID"],
tablefmt="plain",
numalign="left",
stralign="left",
)
)


@auth.command(hidden=True)
@click.option("--id", "-i", required=True, help="A token ID.", hidden=True)
def revoke_token(id):
"""
Revote a Prefect Cloud API token

\b
Options:
--id, -i TEXT The id of a token to revoke
"""
check_override_auth_token()

client = Client()

output = client.graphql(
query={
"mutation($input: deleteAPITokenInput!)": {
"deleteAPIToken(input: $input)": {"success"}
}
},
variables=dict(input=dict(tokenId=id)),
)

if not output.get("data", None) or not output.data.deleteAPIToken.success:
click.secho("Unable to revoke token with ID {}".format(id), fg="red")
return

click.secho("Token successfully revoked", fg="green")
Loading