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 CLI user commands #2153

Merged
merged 33 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d573486
Add CLI login
ThomasLaPiana Jan 6, 2023
8ac6d55
feat: 'fides user login' writes the root access token to a .credentia…
ThomasLaPiana Jan 6, 2023
ceb6c37
feat: add username/password options to the login command
ThomasLaPiana Jan 6, 2023
8c5c1f5
feat: store .credentials in the user dir
ThomasLaPiana Jan 6, 2023
54d6673
refactor: cleanup login code
ThomasLaPiana Jan 6, 2023
b41233b
feat: start stubbing out a user creation command
ThomasLaPiana Jan 6, 2023
942f241
feat: update how "login" works and add a "Credentials" model
ThomasLaPiana Jan 9, 2023
73a50b4
fix: get user creation working
ThomasLaPiana Jan 9, 2023
3c3b1ae
feat: get user creation working, automatically sets all scopes as per…
ThomasLaPiana Jan 9, 2023
3deb77b
fix: login command uses user login instead of client login
ThomasLaPiana Jan 9, 2023
26b709d
feat: update the credentials file to also contain the user id
ThomasLaPiana Jan 9, 2023
8261a7f
feat: get user permissions listing working for users
ThomasLaPiana Jan 9, 2023
3c33281
refactor: organize CLI tests into classes and update default pytest o…
ThomasLaPiana Jan 9, 2023
07db01f
feat: add tests for the user CLI commands, known not passing
ThomasLaPiana Jan 9, 2023
02a0af3
feat: add the first test for the new user module
ThomasLaPiana Jan 9, 2023
8b9f973
fix: static checks
ThomasLaPiana Jan 10, 2023
6799f9a
fix: add b64 encode to password
ThomasLaPiana Jan 10, 2023
3bd1dfb
feat: print the newly logged-in user
ThomasLaPiana Jan 10, 2023
da58d27
fix: pass in the server_url instead of hardcoding it
ThomasLaPiana Jan 10, 2023
3ea2e86
fix: cli user tests
ThomasLaPiana Jan 10, 2023
7429c81
fix: static checks
ThomasLaPiana Jan 10, 2023
436fc90
feat: prompt the user for username/password if not provided and obscu…
ThomasLaPiana Jan 11, 2023
661d6a5
fix: add the functionality from the last commit
ThomasLaPiana Jan 11, 2023
0ea7672
refactor: move custom user options into options.py
ThomasLaPiana Jan 11, 2023
eb48830
refactor: write_credentials_file takes a Credentials object
ThomasLaPiana Jan 11, 2023
57ad1a9
fix: update user CLI tests to use a temp dir
ThomasLaPiana Jan 11, 2023
2fdcf6c
feat: check for credentials path in env var
ThomasLaPiana Jan 11, 2023
33a8f08
refactor: move all command logic from the CLI commands to standard fu…
ThomasLaPiana Jan 11, 2023
14125f5
docs: update user command docstrings
ThomasLaPiana Jan 11, 2023
18bf535
fix: erroneous compose file change
ThomasLaPiana Jan 11, 2023
639f202
fix: static checks
ThomasLaPiana Jan 11, 2023
8fd2c96
docs: slight docstring tweak
ThomasLaPiana Jan 11, 2023
b286fae
docs: updated changelog
ThomasLaPiana Jan 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore

# Files to keep that would otherwise get ignored

# frontend
ui-build/

Expand Down
2 changes: 2 additions & 0 deletions src/fides/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .commands.scan import scan
from .commands.util import deploy, init, status, webserver, worker
from .commands.view import view
from .commands.user import user

CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
LOCAL_COMMANDS = [deploy, evaluate, generate, init, scan, parse, view, webserver]
Expand All @@ -36,6 +37,7 @@
pull,
push,
worker,
user,
]
API_COMMAND_DICT = {command.name or str(command): command for command in API_COMMANDS}
ALL_COMMANDS = API_COMMANDS + LOCAL_COMMANDS
Expand Down
101 changes: 101 additions & 0 deletions src/fides/cli/commands/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Contains the user group of commands for fides."""

import click
from typing import Dict, List

from fides.core.user import (
create_user,
get_access_token,
write_credentials_file,
CREDENTIALS_PATH,
get_user_scopes,
create_auth_header,
read_credentials_file,
)
from fides.core.utils import echo_green


@click.group(name="user")
@click.pass_context
def user(ctx: click.Context) -> None:
"""
Click command group for interacting with user-related functionality.
"""


@user.command()
@click.pass_context
@click.argument("username", type=str)
@click.argument("password", type=str)
ThomasLaPiana marked this conversation as resolved.
Show resolved Hide resolved
@click.option(
"-f",
"--first-name",
default="",
help="First name of the user.",
)
@click.option(
"-l",
"--last-name",
default="",
help="Last name of the user.",
)
def create(
ctx: click.Context, username: str, password: str, first_name: str, last_name: str
) -> None:
"""Use credentials to get a user access token and write a credentials file."""

# If no username/password was provided, attempt to use the root
config = ctx.obj["CONFIG"]
client_id = config.security.oauth_root_client_id
client_secret = config.security.oauth_root_client_secret
access_token = get_access_token(client_id, client_secret)
auth_header = create_auth_header(access_token)

create_user(username, password, first_name, last_name, auth_header)


@user.command()
ThomasLaPiana marked this conversation as resolved.
Show resolved Hide resolved
@click.pass_context
@click.option(
"-u",
"--username",
default="",
help="Username to authenticate with.",
)
@click.option(
"-p",
"--password",
default="",
help="Password to authenticate with.",
)
def login(ctx: click.Context, username: str, password: str) -> None:
"""Use credentials to get a user access token and write a credentials file."""

# If no username/password was provided, attempt to use the root
config = ctx.obj["CONFIG"]
username = username or config.security.oauth_root_client_id
password = password or config.security.oauth_root_client_secret

access_token = get_access_token(username, password)
write_credentials_file(username, access_token)
echo_green(f"Credentials file written to: {CREDENTIALS_PATH}")


@user.command()
@click.option(
"-u",
"--username",
default="",
help="Username to authenticate with.",
)
def scopes(username: str) -> None:
"""List the scopes avaible to the current user."""

credentials: Dict[str, str] = read_credentials_file()
username = credentials["username"]
access_token = credentials["access_token"]

auth_header = create_auth_header(access_token)
scopes: List[str] = get_user_scopes(username, auth_header)
for scope in scopes:
print(scope)
93 changes: 93 additions & 0 deletions src/fides/core/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Module for interaction with User endpoints/commands."""
from pathlib import Path
from typing import Dict, List

import requests
import toml

from fides.core.utils import echo_red

OAUTH_TOKEN_URL = "http://localhost:8080/api/v1/oauth/token"
CLIENT_SCOPES_URL = "http://localhost:8080/api/v1/oauth/client/{}/scope"
CREDENTIALS_PATH = f"{str(Path.home())}/.fides_credentials"


def get_access_token(username: str, password: str) -> str:
"""
Get a user access token from the webserver.
"""
payload = {
"client_id": username,
"client_secret": password,
"grant_type": "client_credentials",
}

response = requests.post(OAUTH_TOKEN_URL, data=payload)
if response.status_code == 401:
echo_red(
"Authentication failed! Please check your username/password and try again."
)
raise SystemExit(1)
access_token = response.json()["access_token"]
return access_token


def write_credentials_file(username: str, access_token: str) -> str:
"""
Write the user credentials file.
"""
credentials_data = {"username": username, "access_token": access_token}
with open(CREDENTIALS_PATH, "w", encoding="utf-8") as credentials_file:
credentials_file.write(toml.dumps(credentials_data))
return CREDENTIALS_PATH


def read_credentials_file() -> Dict[str, str]:
"""Read and return the credentials file."""
with open(CREDENTIALS_PATH, "r", encoding="utf-8") as credentials_file:
credentials_data = toml.load(credentials_file)
return credentials_data


def create_auth_header(access_token: str) -> Dict[str, str]:
"""Given an access token, create an auth header."""
auth_header = {"Authorization": f"Bearer {access_token}"}
return auth_header


def get_user_scopes(username: str, auth_header: Dict[str, str]) -> List[str]:
"""
Get a user access token from the webserver.
"""
scopes_url = CLIENT_SCOPES_URL.format(username)
response = requests.get(
scopes_url,
headers=auth_header,
)

if response.status_code != 200:
echo_red("Request failed! Please check your username/password and try again.")
raise SystemExit(1)

return response.json()


def create_user(
username: str,
password: str,
first_name: str,
last_name: str,
auth_header: Dict[str, str],
) -> None:
"""Create a user."""
user_data = {
"username": username,
"password": password,
"first_name": first_name,
"last_name": last_name,
}
print(user_data)
Fixed Show fixed Hide fixed
response = requests.post(
"http://localhost:8080/api/v1/user", headers=auth_header, data=user_data
)
print(response.text)