From d573486d3e226dd0e2ba1c90e901eada827b38e1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 5 Jan 2023 21:29:17 -0800 Subject: [PATCH 01/33] Add CLI login --- src/fides/cli/commands/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fides/cli/commands/util.py b/src/fides/cli/commands/util.py index 38fb4a5e1b..f3429e36cb 100644 --- a/src/fides/cli/commands/util.py +++ b/src/fides/cli/commands/util.py @@ -54,6 +54,8 @@ def init(ctx: click.Context, fides_directory_location: str) -> None: send_init_analytics(config.user.analytics_opt_out, config_path, executed_at) echo_green("fides initialization complete.") + +# Add a login command @click.command() From 8ac6d55144432ca2654b2e9aaf957e194a9711c3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 6 Jan 2023 14:00:05 +0800 Subject: [PATCH 02/33] feat: 'fides user login' writes the root access token to a .credentials file --- .gitignore | 2 +- src/fides/cli/__init__.py | 2 ++ src/fides/cli/commands/user.py | 39 ++++++++++++++++++++++++++++++++++ src/fides/cli/commands/util.py | 2 -- 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/fides/cli/commands/user.py diff --git a/.gitignore b/.gitignore index 5ae51ec915..1cb6b8f658 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore -# Files to keep that would otherwise get ignored +.credentials # frontend ui-build/ diff --git a/src/fides/cli/__init__.py b/src/fides/cli/__init__.py index 255e2ada7f..bf6f4edc58 100644 --- a/src/fides/cli/__init__.py +++ b/src/fides/cli/__init__.py @@ -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] @@ -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 diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py new file mode 100644 index 0000000000..730f9a23bd --- /dev/null +++ b/src/fides/cli/commands/user.py @@ -0,0 +1,39 @@ +"""Contains the user group of commands for fides.""" + +import click + +import requests +import toml + + +@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 +def login(ctx: click.Context) -> None: + """Use credentials to verify a user.""" + + # If no username/password was provided, attempt to use the root + config = ctx.obj["CONFIG"] + username = config.security.oauth_root_client_id + password = config.security.oauth_root_client_secret + payload = { + "client_id": username, + "client_secret": password, + "grant_type": "client_credentials", + } + + # Hit the auth endpoint + response = requests.post("http://localhost:8080/api/v1/oauth/token", data=payload) + access_token = response.json()["access_token"] + + # Store the token in a .credentials file + credentials_data = {"access_token": access_token} + with open(".credentials", "w", encoding="utf-8") as credentials_file: + credentials_file.write(toml.dumps(credentials_data)) diff --git a/src/fides/cli/commands/util.py b/src/fides/cli/commands/util.py index f3429e36cb..38fb4a5e1b 100644 --- a/src/fides/cli/commands/util.py +++ b/src/fides/cli/commands/util.py @@ -54,8 +54,6 @@ def init(ctx: click.Context, fides_directory_location: str) -> None: send_init_analytics(config.user.analytics_opt_out, config_path, executed_at) echo_green("fides initialization complete.") - -# Add a login command @click.command() From ceb6c37f04cef7d54ebebadf50d3f2bf5cb83cc6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 6 Jan 2023 14:14:28 +0800 Subject: [PATCH 03/33] feat: add username/password options to the login command --- src/fides/cli/commands/user.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 730f9a23bd..86375f7682 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -5,6 +5,8 @@ import requests import toml +from fides.core.utils import echo_red + @click.group(name="user") @click.pass_context @@ -16,24 +18,42 @@ def user(ctx: click.Context) -> None: @user.command() @click.pass_context -def login(ctx: click.Context) -> None: - """Use credentials to verify a user.""" +@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.""" # If no username/password was provided, attempt to use the root config = ctx.obj["CONFIG"] - username = config.security.oauth_root_client_id - password = config.security.oauth_root_client_secret + username = username or config.security.oauth_root_client_id + password = password or config.security.oauth_root_client_secret payload = { "client_id": username, "client_secret": password, "grant_type": "client_credentials", } - # Hit the auth endpoint + # Get the access token from the auth endpoint response = requests.post("http://localhost:8080/api/v1/oauth/token", data=payload) + if response.status_code != 200: + echo_red("Authentication failed!") + raise SystemExit(1) access_token = response.json()["access_token"] # Store the token in a .credentials file credentials_data = {"access_token": access_token} - with open(".credentials", "w", encoding="utf-8") as credentials_file: + credentials_path = ".credentials" + print(f"Writing credentials to: '{credentials_path}'...") + with open(credentials_path, "w", encoding="utf-8") as credentials_file: credentials_file.write(toml.dumps(credentials_data)) + print("Credentials successfully created.") From 8c5c1f56d9e98dc47f9cb08e354845cd174b2db7 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 6 Jan 2023 14:20:31 +0800 Subject: [PATCH 04/33] feat: store .credentials in the user dir --- .gitignore | 2 -- src/fides/cli/commands/user.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1cb6b8f658..505e771b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore -.credentials - # frontend ui-build/ diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 86375f7682..2d2cbd990b 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -1,6 +1,7 @@ """Contains the user group of commands for fides.""" import click +from pathlib import Path import requests import toml @@ -52,7 +53,8 @@ def login(ctx: click.Context, username: str, password: str) -> None: # Store the token in a .credentials file credentials_data = {"access_token": access_token} - credentials_path = ".credentials" + credentials_path = f"{str(Path.home())}/.credentials" + print(f"Writing credentials to: '{credentials_path}'...") with open(credentials_path, "w", encoding="utf-8") as credentials_file: credentials_file.write(toml.dumps(credentials_data)) From 54d6673c240f08c2e26d93f3107aa1089795e568 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 6 Jan 2023 23:33:42 +0800 Subject: [PATCH 05/33] refactor: cleanup login code --- src/fides/cli/commands/user.py | 64 ++++++++++++++++++------------- src/fides/core/user.py | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/fides/core/user.py diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 2d2cbd990b..9333fb6823 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -1,12 +1,17 @@ """Contains the user group of commands for fides.""" import click -from pathlib import Path +from typing import Dict, List -import requests -import toml - -from fides.core.utils import echo_red +from fides.core.user import ( + 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") @@ -32,30 +37,35 @@ def user(ctx: click.Context) -> None: help="Password to authenticate with.", ) def login(ctx: click.Context, username: str, password: str) -> None: - """Use credentials to get a user access token.""" + """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 - payload = { - "client_id": username, - "client_secret": password, - "grant_type": "client_credentials", - } - - # Get the access token from the auth endpoint - response = requests.post("http://localhost:8080/api/v1/oauth/token", data=payload) - if response.status_code != 200: - echo_red("Authentication failed!") - raise SystemExit(1) - access_token = response.json()["access_token"] - - # Store the token in a .credentials file - credentials_data = {"access_token": access_token} - credentials_path = f"{str(Path.home())}/.credentials" - - print(f"Writing credentials to: '{credentials_path}'...") - with open(credentials_path, "w", encoding="utf-8") as credentials_file: - credentials_file.write(toml.dumps(credentials_data)) - print("Credentials successfully created.") + + 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.pass_context +@click.option( + "-u", + "--username", + default="", + help="Username to authenticate with.", +) +def scopes(ctx: click.Context, username: str) -> None: + """List the scopes avaible to the current user.""" + + config = ctx.obj["CONFIG"] + + 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) + print(scopes) diff --git a/src/fides/core/user.py b/src/fides/core/user.py new file mode 100644 index 0000000000..84c9dbce63 --- /dev/null +++ b/src/fides/core/user.py @@ -0,0 +1,69 @@ +"""Module for interaction with User endpoints/commands.""" +from pathlib import Path +from typing import Dict + +import requests +import toml + +from fides.core.utils import echo_red + +OAUTH_TOKEN_URL = "http://localhost:8080/api/v1/oauth/token" +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 != 200: + 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) -> str: + """Given an access token, create an auth header.""" + auth_header = f"Authorization: Bearer {access_token}" + return auth_header + + +def get_user_scopes(username: str, auth_header: str) -> str: + """ + Get a user access token from the webserver. + """ + response = requests.post( + f"http://localhost:8080/api/v1/oauth/{username}/scope", headers=auth_header + ) + if response.status_code != 200: + echo_red( + "Authentication failed! Please check your username/password and try again." + ) + raise SystemExit(1) + print(response.json()) From b41233b990d7116c5a95b9f6d71ddc462da03c4d Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 7 Jan 2023 00:42:11 +0800 Subject: [PATCH 06/33] feat: start stubbing out a user creation command --- src/fides/cli/commands/user.py | 40 +++++++++++++++++++++++++---- src/fides/core/user.py | 46 ++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 9333fb6823..849e0ae280 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -4,6 +4,7 @@ from typing import Dict, List from fides.core.user import ( + create_user, get_access_token, write_credentials_file, CREDENTIALS_PATH, @@ -22,6 +23,37 @@ def user(ctx: click.Context) -> None: """ +@user.command() +@click.pass_context +@click.argument("username", type=str) +@click.argument("password", type=str) +@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() @click.pass_context @click.option( @@ -50,22 +82,20 @@ def login(ctx: click.Context, username: str, password: str) -> None: @user.command() -@click.pass_context @click.option( "-u", "--username", default="", help="Username to authenticate with.", ) -def scopes(ctx: click.Context, username: str) -> None: +def scopes(username: str) -> None: """List the scopes avaible to the current user.""" - config = ctx.obj["CONFIG"] - 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) - print(scopes) + for scope in scopes: + print(scope) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 84c9dbce63..2cbea2cb3e 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -1,6 +1,6 @@ """Module for interaction with User endpoints/commands.""" from pathlib import Path -from typing import Dict +from typing import Dict, List import requests import toml @@ -8,6 +8,7 @@ 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" @@ -22,7 +23,7 @@ def get_access_token(username: str, password: str) -> str: } response = requests.post(OAUTH_TOKEN_URL, data=payload) - if response.status_code != 200: + if response.status_code == 401: echo_red( "Authentication failed! Please check your username/password and try again." ) @@ -48,22 +49,45 @@ def read_credentials_file() -> Dict[str, str]: return credentials_data -def create_auth_header(access_token: str) -> str: +def create_auth_header(access_token: str) -> Dict[str, str]: """Given an access token, create an auth header.""" - auth_header = f"Authorization: Bearer {access_token}" + auth_header = {"Authorization": f"Bearer {access_token}"} return auth_header -def get_user_scopes(username: str, auth_header: str) -> str: +def get_user_scopes(username: str, auth_header: Dict[str, str]) -> List[str]: """ Get a user access token from the webserver. """ - response = requests.post( - f"http://localhost:8080/api/v1/oauth/{username}/scope", headers=auth_header + scopes_url = CLIENT_SCOPES_URL.format(username) + response = requests.get( + scopes_url, + headers=auth_header, ) + if response.status_code != 200: - echo_red( - "Authentication failed! Please check your username/password and try again." - ) + echo_red("Request failed! Please check your username/password and try again.") raise SystemExit(1) - print(response.json()) + + 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) + response = requests.post( + "http://localhost:8080/api/v1/user", headers=auth_header, data=user_data + ) + print(response.text) From 942f2419a9f195e5f1876288b0a03d8ae6bc6135 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 12:27:02 +0800 Subject: [PATCH 07/33] feat: update how "login" works and add a "Credentials" model --- src/fides/cli/commands/user.py | 33 +++++++++++++++++++++------------ src/fides/core/user.py | 21 ++++++++++++++++++--- tests/ctl/core/test_users.py | 3 +++ 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 tests/ctl/core/test_users.py diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 849e0ae280..08d49080c8 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -12,7 +12,7 @@ create_auth_header, read_credentials_file, ) -from fides.core.utils import echo_green +from fides.core.utils import echo_green, echo_red @click.group(name="user") @@ -42,9 +42,10 @@ def user(ctx: click.Context) -> None: 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.""" + """ + Use credentials from the credentials file to create a new user. + """ - # 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 @@ -55,7 +56,6 @@ def create( @user.command() -@click.pass_context @click.option( "-u", "--username", @@ -68,16 +68,25 @@ def create( 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.""" +def login(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 + If no username/password is provided, attempt to load an existing credentials file + and use that username/password. + """ + + if not (username and password): + try: + credentials: Dict[str, str] = read_credentials_file() + except FileNotFoundError: + echo_red("No username/password provided and no credentials file found.") + raise SystemExit(1) + username = credentials["username"] + password = credentials["password"] access_token = get_access_token(username, password) - write_credentials_file(username, access_token) + write_credentials_file(username, password, access_token) echo_green(f"Credentials file written to: {CREDENTIALS_PATH}") @@ -86,7 +95,7 @@ def login(ctx: click.Context, username: str, password: str) -> None: "-u", "--username", default="", - help="Username to authenticate with.", + help="Username to get scopes for.", ) def scopes(username: str) -> None: """List the scopes avaible to the current user.""" diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 2cbea2cb3e..9d8ad2bc3f 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -1,6 +1,7 @@ """Module for interaction with User endpoints/commands.""" from pathlib import Path from typing import Dict, List +from pydantic import BaseModel import requests import toml @@ -12,6 +13,16 @@ CREDENTIALS_PATH = f"{str(Path.home())}/.fides_credentials" +class Credentials(BaseModel): + """ + User credentials for the CLI. + """ + + username: str + password: str + access_token: str + + def get_access_token(username: str, password: str) -> str: """ Get a user access token from the webserver. @@ -32,13 +43,17 @@ def get_access_token(username: str, password: str) -> str: return access_token -def write_credentials_file(username: str, access_token: str) -> str: +def write_credentials_file(username: str, password: str, access_token: str) -> str: """ Write the user credentials file. """ - credentials_data = {"username": username, "access_token": access_token} + credentials = Credentials( + username=username, + password=password, + access_token=access_token, + ) with open(CREDENTIALS_PATH, "w", encoding="utf-8") as credentials_file: - credentials_file.write(toml.dumps(credentials_data)) + credentials_file.write(toml.dumps(credentials.dict())) return CREDENTIALS_PATH diff --git a/tests/ctl/core/test_users.py b/tests/ctl/core/test_users.py new file mode 100644 index 0000000000..739f349bad --- /dev/null +++ b/tests/ctl/core/test_users.py @@ -0,0 +1,3 @@ +import pytest + +# Test each function From 73a50b4ad03acf359728d99a5c9b057a5172bd55 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 12:38:40 +0800 Subject: [PATCH 08/33] fix: get user creation working --- src/fides/cli/commands/user.py | 12 +++++++----- src/fides/core/user.py | 13 +++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 08d49080c8..362550e8c6 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -46,12 +46,14 @@ def create( Use credentials from the credentials file to create a new user. """ - 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) + try: + credentials = read_credentials_file() + except FileNotFoundError: + echo_red("No credentials file found.") + raise SystemExit(1) + access_token = credentials.access_token + auth_header = create_auth_header(access_token) create_user(username, password, first_name, last_name, auth_header) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 9d8ad2bc3f..3e5f1a42eb 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -57,16 +57,18 @@ def write_credentials_file(username: str, password: str, access_token: str) -> s return CREDENTIALS_PATH -def read_credentials_file() -> Dict[str, str]: +def read_credentials_file() -> Credentials: """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 + credentials = Credentials.parse_obj(toml.load(credentials_file)) + return credentials 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}"} + auth_header = { + "Authorization": f"Bearer {access_token}", + } return auth_header @@ -101,8 +103,7 @@ def create_user( "first_name": first_name, "last_name": last_name, } - print(user_data) response = requests.post( - "http://localhost:8080/api/v1/user", headers=auth_header, data=user_data + "http://localhost:8080/api/v1/user", headers=auth_header, json=user_data ) print(response.text) From 3c3b1ae20c4b7d360b54c1200d9bc4c40df05489 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 13:35:16 +0800 Subject: [PATCH 09/33] feat: get user creation working, automatically sets all scopes as permissions --- src/fides/cli/commands/user.py | 25 +++++++++++--------- src/fides/core/user.py | 43 +++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 362550e8c6..d23b024bb2 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -11,8 +11,10 @@ get_user_scopes, create_auth_header, read_credentials_file, + update_user_permissions, ) from fides.core.utils import echo_green, echo_red +from fides.lib.oauth.scopes import SCOPES @click.group(name="user") @@ -24,7 +26,6 @@ def user(ctx: click.Context) -> None: @user.command() -@click.pass_context @click.argument("username", type=str) @click.argument("password", type=str) @click.option( @@ -39,11 +40,11 @@ def user(ctx: click.Context) -> None: default="", help="Last name of the user.", ) -def create( - ctx: click.Context, username: str, password: str, first_name: str, last_name: str -) -> None: +def create(username: str, password: str, first_name: str, last_name: str) -> None: """ Use credentials from the credentials file to create a new user. + + Gives all scopes as permissions. """ try: @@ -54,7 +55,9 @@ def create( access_token = credentials.access_token auth_header = create_auth_header(access_token) - create_user(username, password, first_name, last_name, auth_header) + user_response = create_user(username, password, first_name, last_name, auth_header) + user_id = user_response.json()["id"] + update_user_permissions(user_id=user_id, scopes=SCOPES, auth_header=auth_header) @user.command() @@ -80,12 +83,12 @@ def login(username: str, password: str) -> None: if not (username and password): try: - credentials: Dict[str, str] = read_credentials_file() + credentials = read_credentials_file() except FileNotFoundError: echo_red("No username/password provided and no credentials file found.") raise SystemExit(1) - username = credentials["username"] - password = credentials["password"] + username = credentials.username + password = credentials.password access_token = get_access_token(username, password) write_credentials_file(username, password, access_token) @@ -102,9 +105,9 @@ def login(username: str, password: str) -> None: 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"] + credentials = 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) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 3e5f1a42eb..546c4cad10 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -2,11 +2,12 @@ from pathlib import Path from typing import Dict, List from pydantic import BaseModel +import json import requests import toml -from fides.core.utils import echo_red +from fides.cli.utils import handle_cli_response OAUTH_TOKEN_URL = "http://localhost:8080/api/v1/oauth/token" CLIENT_SCOPES_URL = "http://localhost:8080/api/v1/oauth/client/{}/scope" @@ -34,11 +35,7 @@ def get_access_token(username: str, password: str) -> str: } 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) + handle_cli_response(response) access_token = response.json()["access_token"] return access_token @@ -68,6 +65,8 @@ 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}", + "accept": "application/json", + "Content-Type": "application/json", } return auth_header @@ -82,10 +81,7 @@ def get_user_scopes(username: str, auth_header: Dict[str, str]) -> List[str]: headers=auth_header, ) - if response.status_code != 200: - echo_red("Request failed! Please check your username/password and try again.") - raise SystemExit(1) - + handle_cli_response(response) return response.json() @@ -95,15 +91,34 @@ def create_user( first_name: str, last_name: str, auth_header: Dict[str, str], -) -> None: +) -> requests.Response: """Create a user.""" - user_data = { + request_data = { "username": username, "password": password, "first_name": first_name, "last_name": last_name, } response = requests.post( - "http://localhost:8080/api/v1/user", headers=auth_header, json=user_data + "http://localhost:8080/api/v1/user", + headers=auth_header, + data=json.dumps(request_data), + ) + handle_cli_response(response) + return response + + +def update_user_permissions( + user_id: str, scopes: List[str], auth_header: Dict[str, str] +) -> requests.Response: + """ + Update user permissions for a given user. + """ + request_data = {"scopes": scopes, "id": user_id} + response = requests.put( + f"http://localhost:8080/api/v1/user/{user_id}/permission", + headers=auth_header, + json=request_data, ) - print(response.text) + handle_cli_response(response) + return response From 3deb77bd410fab9da0579c7c1a36a48dde9336bf Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 14:10:36 +0800 Subject: [PATCH 10/33] fix: login command uses user login instead of client login --- src/fides/core/user.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 546c4cad10..e4a4a7c183 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -9,8 +9,10 @@ from fides.cli.utils import handle_cli_response -OAUTH_TOKEN_URL = "http://localhost:8080/api/v1/oauth/token" CLIENT_SCOPES_URL = "http://localhost:8080/api/v1/oauth/client/{}/scope" +LOGIN_URL = "http://localhost:8080/api/v1/login" +USER_PERMISSION_URL = "http://localhost:8080/api/v1/user/{}/permission" + CREDENTIALS_PATH = f"{str(Path.home())}/.fides_credentials" @@ -29,14 +31,13 @@ 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", + "username": username, + "password": password, } - response = requests.post(OAUTH_TOKEN_URL, data=payload) - handle_cli_response(response) - access_token = response.json()["access_token"] + response = requests.post(LOGIN_URL, json=payload) + handle_cli_response(response, verbose=False) + access_token = response.json()["token_data"]["access_token"] return access_token @@ -65,8 +66,6 @@ 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}", - "accept": "application/json", - "Content-Type": "application/json", } return auth_header @@ -81,7 +80,7 @@ def get_user_scopes(username: str, auth_header: Dict[str, str]) -> List[str]: headers=auth_header, ) - handle_cli_response(response) + handle_cli_response(response, verbose=False) return response.json() @@ -104,7 +103,7 @@ def create_user( headers=auth_header, data=json.dumps(request_data), ) - handle_cli_response(response) + handle_cli_response(response, verbose=False) return response @@ -116,9 +115,9 @@ def update_user_permissions( """ request_data = {"scopes": scopes, "id": user_id} response = requests.put( - f"http://localhost:8080/api/v1/user/{user_id}/permission", + USER_PERMISSION_URL.format(user_id), headers=auth_header, json=request_data, ) - handle_cli_response(response) + handle_cli_response(response, verbose=False) return response From 26b709d8db2d0e99e5b62456d5a0e7f9ebe08bfd Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 14:28:05 +0800 Subject: [PATCH 11/33] feat: update the credentials file to also contain the user id --- src/fides/cli/commands/user.py | 15 +++++----- src/fides/core/user.py | 51 +++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index d23b024bb2..cfeaa9ce82 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -1,14 +1,14 @@ """Contains the user group of commands for fides.""" import click -from typing import Dict, List +from typing import List from fides.core.user import ( create_user, get_access_token, write_credentials_file, CREDENTIALS_PATH, - get_user_scopes, + get_user_permissions, create_auth_header, read_credentials_file, update_user_permissions, @@ -44,7 +44,7 @@ def create(username: str, password: str, first_name: str, last_name: str) -> Non """ Use credentials from the credentials file to create a new user. - Gives all scopes as permissions. + Gives full permissions. """ try: @@ -58,6 +58,7 @@ def create(username: str, password: str, first_name: str, last_name: str) -> Non user_response = create_user(username, password, first_name, last_name, auth_header) user_id = user_response.json()["id"] update_user_permissions(user_id=user_id, scopes=SCOPES, auth_header=auth_header) + echo_green(f"User: '{username}' created and assigned permissions.") @user.command() @@ -90,8 +91,8 @@ def login(username: str, password: str) -> None: username = credentials.username password = credentials.password - access_token = get_access_token(username, password) - write_credentials_file(username, password, access_token) + user_id, access_token = get_access_token(username, password) + write_credentials_file(username, password, user_id, access_token) echo_green(f"Credentials file written to: {CREDENTIALS_PATH}") @@ -102,7 +103,7 @@ def login(username: str, password: str) -> None: default="", help="Username to get scopes for.", ) -def scopes(username: str) -> None: +def permissions(username: str) -> None: """List the scopes avaible to the current user.""" credentials = read_credentials_file() @@ -110,6 +111,6 @@ def scopes(username: str) -> None: access_token = credentials.access_token auth_header = create_auth_header(access_token) - scopes: List[str] = get_user_scopes(username, auth_header) + scopes: List[str] = get_user_permissions(username, auth_header) for scope in scopes: print(scope) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index e4a4a7c183..a2d04c55b8 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -1,6 +1,6 @@ """Module for interaction with User endpoints/commands.""" from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Tuple from pydantic import BaseModel import json @@ -9,9 +9,9 @@ from fides.cli.utils import handle_cli_response -CLIENT_SCOPES_URL = "http://localhost:8080/api/v1/oauth/client/{}/scope" +CREATE_USER_URL = "http://localhost:8080/api/v1/user" LOGIN_URL = "http://localhost:8080/api/v1/login" -USER_PERMISSION_URL = "http://localhost:8080/api/v1/user/{}/permission" +USER_PERMISSIONS_URL = "http://localhost:8080/api/v1/user/{}/permission" CREDENTIALS_PATH = f"{str(Path.home())}/.fides_credentials" @@ -23,10 +23,11 @@ class Credentials(BaseModel): username: str password: str + user_id: str access_token: str -def get_access_token(username: str, password: str) -> str: +def get_access_token(username: str, password: str) -> Tuple[str, str]: """ Get a user access token from the webserver. """ @@ -37,17 +38,21 @@ def get_access_token(username: str, password: str) -> str: response = requests.post(LOGIN_URL, json=payload) handle_cli_response(response, verbose=False) - access_token = response.json()["token_data"]["access_token"] - return access_token + user_id: str = response.json()["user_data"]["id"] + access_token: str = response.json()["token_data"]["access_token"] + return (user_id, access_token) -def write_credentials_file(username: str, password: str, access_token: str) -> str: +def write_credentials_file( + username: str, password: str, user_id: str, access_token: str +) -> str: """ Write the user credentials file. """ credentials = Credentials( username=username, password=password, + user_id=user_id, access_token=access_token, ) with open(CREDENTIALS_PATH, "w", encoding="utf-8") as credentials_file: @@ -70,20 +75,6 @@ def create_auth_header(access_token: str) -> Dict[str, str]: 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, - ) - - handle_cli_response(response, verbose=False) - return response.json() - - def create_user( username: str, password: str, @@ -99,7 +90,7 @@ def create_user( "last_name": last_name, } response = requests.post( - "http://localhost:8080/api/v1/user", + CREATE_USER_URL, headers=auth_header, data=json.dumps(request_data), ) @@ -107,6 +98,20 @@ def create_user( return response +def get_user_permissions(username: str, auth_header: Dict[str, str]) -> List[str]: + """ + List all of the user permissions for the provided user. + """ + scopes_url = USER_PERMISSIONS_URL.format(username) + response = requests.get( + scopes_url, + headers=auth_header, + ) + + handle_cli_response(response, verbose=False) + return response.json() + + def update_user_permissions( user_id: str, scopes: List[str], auth_header: Dict[str, str] ) -> requests.Response: @@ -115,7 +120,7 @@ def update_user_permissions( """ request_data = {"scopes": scopes, "id": user_id} response = requests.put( - USER_PERMISSION_URL.format(user_id), + USER_PERMISSIONS_URL.format(user_id), headers=auth_header, json=request_data, ) From 8261a7faf90f6b6a345f4d36d12365dc2c2fca1b Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 14:44:14 +0800 Subject: [PATCH 12/33] feat: get user permissions listing working for users --- src/fides/cli/commands/user.py | 13 ++++--------- src/fides/core/user.py | 6 +++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index cfeaa9ce82..8114bde721 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -97,20 +97,15 @@ def login(username: str, password: str) -> None: @user.command() -@click.option( - "-u", - "--username", - default="", - help="Username to get scopes for.", -) -def permissions(username: str) -> None: +def permissions() -> None: """List the scopes avaible to the current user.""" credentials = read_credentials_file() - username = credentials.username + user_id = credentials.user_id access_token = credentials.access_token auth_header = create_auth_header(access_token) - scopes: List[str] = get_user_permissions(username, auth_header) + scopes: List[str] = get_user_permissions(user_id, auth_header) + print("Scopes:") for scope in scopes: print(scope) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index a2d04c55b8..63a6bf4be4 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -98,18 +98,18 @@ def create_user( return response -def get_user_permissions(username: str, auth_header: Dict[str, str]) -> List[str]: +def get_user_permissions(user_id: str, auth_header: Dict[str, str]) -> List[str]: """ List all of the user permissions for the provided user. """ - scopes_url = USER_PERMISSIONS_URL.format(username) + scopes_url = USER_PERMISSIONS_URL.format(user_id) response = requests.get( scopes_url, headers=auth_header, ) handle_cli_response(response, verbose=False) - return response.json() + return response.json()["scopes"] def update_user_permissions( From 3c33281c78a59f4f10a21a9aecaa5a2efc177b90 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 15:32:43 +0800 Subject: [PATCH 13/33] refactor: organize CLI tests into classes and update default pytest options --- pyproject.toml | 6 +- tests/ctl/cli/test_cli.py | 1339 ++++++++++++++++++------------------- 2 files changed, 671 insertions(+), 674 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 624e5f7492..9b12cae301 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,10 +155,8 @@ log_cli=false filterwarnings = "ignore::DeprecationWarning:aiofiles.*:" testpaths="tests" log_level = "INFO" -addopts = ["--cov=fides.core", - "--cov=fides.cli", - "--cov=fides.api", - "--cov-report=term-missing", +addopts = ["--cov=fides", + "-ra", "-vv", "--no-cov-on-fail", "--disable-pytest-warnings"] diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index 26a4eb1464..e0bc360f37 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -65,54 +65,56 @@ def test_parse(test_config_path: str, test_cli_runner: CliRunner) -> None: assert result.exit_code == 0 -@pytest.mark.integration -def test_reset_db(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke(cli, ["-f", test_config_path, "db", "reset", "-y"]) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.integration -def test_init_db(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke(cli, ["-f", test_config_path, "db", "init"]) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.integration -def test_push(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke( - cli, ["-f", test_config_path, "push", "demo_resources/"] - ) - print(result.output) - assert result.exit_code == 0 +class TestDB: + @pytest.mark.integration + def test_reset_db(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke( + cli, ["-f", test_config_path, "db", "reset", "-y"] + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.integration + def test_init_db(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke(cli, ["-f", test_config_path, "db", "init"]) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_dry_push(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke( - cli, ["-f", test_config_path, "push", "--dry", "demo_resources/"] - ) - print(result.output) - assert result.exit_code == 0 +class TestPush: + @pytest.mark.integration + def test_push(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke( + cli, ["-f", test_config_path, "push", "demo_resources/"] + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_diff_push(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke( - cli, ["-f", test_config_path, "push", "--diff", "demo_resources/"] - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_dry_push(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke( + cli, ["-f", test_config_path, "push", "--dry", "demo_resources/"] + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.integration + def test_diff_push(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke( + cli, ["-f", test_config_path, "push", "--diff", "demo_resources/"] + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_dry_diff_push(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke( - cli, ["-f", test_config_path, "push", "--dry", "--diff", "demo_resources/"] - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_dry_diff_push( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, ["-f", test_config_path, "push", "--dry", "--diff", "demo_resources/"] + ) + print(result.output) + assert result.exit_code == 0 @pytest.mark.integration @@ -169,678 +171,675 @@ def test_audit(test_config_path: str, test_cli_runner: CliRunner) -> None: assert result.exit_code == 0 -@pytest.mark.integration -def test_get(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke( - cli, - ["-f", test_config_path, "get", "data_category", "user"], - ) - print(result.output) - assert result.exit_code == 0 +class TestCRUD: + @pytest.mark.integration + def test_get(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke( + cli, + ["-f", test_config_path, "get", "data_category", "user"], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.integration + def test_ls(self, test_config_path: str, test_cli_runner: CliRunner) -> None: + result = test_cli_runner.invoke(cli, ["-f", test_config_path, "ls", "system"]) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_ls(test_config_path: str, test_cli_runner: CliRunner) -> None: - result = test_cli_runner.invoke(cli, ["-f", test_config_path, "ls", "system"]) - print(result.output) - assert result.exit_code == 0 +class TestEvaluate: + @pytest.mark.integration + def test_evaluate_with_declaration_pass( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "tests/ctl/data/passing_declaration_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_evaluate_with_declaration_pass( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "evaluate", - "tests/ctl/data/passing_declaration_taxonomy.yml", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_evaluate_demo_resources_pass( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + ["-f", test_config_path, "evaluate", "demo_resources/"], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.integration + def test_local_evaluate( + self, test_invalid_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "--local", + "-f", + test_invalid_config_path, + "evaluate", + "tests/ctl/data/passing_declaration_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_evaluate_demo_resources_pass( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - ["-f", test_config_path, "evaluate", "demo_resources/"], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_local_evaluate_demo_resources( + self, test_invalid_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "--local", + "-f", + test_invalid_config_path, + "evaluate", + "demo_resources/", + ], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.integration + def test_evaluate_with_key_pass( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "-k", + "primary_privacy_policy", + "tests/ctl/data/passing_declaration_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_local_evaluate( - test_invalid_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "--local", - "-f", - test_invalid_config_path, - "evaluate", - "tests/ctl/data/passing_declaration_taxonomy.yml", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_evaluate_with_declaration_failed( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "tests/ctl/data/failing_declaration_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 1 + @pytest.mark.integration + def test_evaluate_with_dataset_failed( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "tests/ctl/data/failing_dataset_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 1 -@pytest.mark.integration -def test_local_evaluate_demo_resources( - test_invalid_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "--local", - "-f", - test_invalid_config_path, - "evaluate", - "demo_resources/", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_evaluate_with_dataset_field_failed( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "tests/ctl/data/failing_dataset_collection_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 1 + @pytest.mark.integration + def test_evaluate_with_dataset_collection_failed( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "tests/ctl/data/failing_dataset_field_taxonomy.yml", + ], + ) + print(result.output) + assert result.exit_code == 1 -@pytest.mark.integration -def test_evaluate_with_key_pass( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "evaluate", - "-k", - "primary_privacy_policy", - "tests/ctl/data/passing_declaration_taxonomy.yml", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_evaluate_nested_field_fails( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + """ + Tests a taxonomy that is rigged to fail only due to + one of the nested fields violating the policy. Test + will fail if the nested field is not discovered. + """ + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "evaluate", + "tests/ctl/data/failing_nested_dataset.yml", + ], + ) + print(result.output) + assert result.exit_code == 1 @pytest.mark.integration -def test_evaluate_with_declaration_failed( - test_config_path: str, test_cli_runner: CliRunner +@pytest.mark.parametrize( + "export_resource", ["system", "dataset", "organization", "datamap"] +) +def test_export_resources( + test_config_path: str, + test_cli_runner: CliRunner, + export_resource: str, ) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "evaluate", - "tests/ctl/data/failing_declaration_taxonomy.yml", - ], - ) - print(result.output) - assert result.exit_code == 1 - + """ + Tests that each resource is successfully exported + """ -@pytest.mark.integration -def test_evaluate_with_dataset_failed( - test_config_path: str, test_cli_runner: CliRunner -) -> None: result = test_cli_runner.invoke( cli, [ "-f", test_config_path, - "evaluate", - "tests/ctl/data/failing_dataset_taxonomy.yml", + "export", + export_resource, + "--dry", ], ) - print(result.output) - assert result.exit_code == 1 - + assert result.exit_code == 0 -@pytest.mark.integration -def test_evaluate_with_dataset_field_failed( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "evaluate", - "tests/ctl/data/failing_dataset_collection_taxonomy.yml", - ], - ) - print(result.output) - assert result.exit_code == 1 +class TestScan: + @pytest.mark.integration + def test_scan_dataset_db_input_connection_string( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "dataset", + "db", + "--connection-string", + "postgresql+psycopg2://postgres:fides@fides-db:5432/fides_test", + "--coverage-threshold", + "0", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.integration -def test_evaluate_with_dataset_collection_failed( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "evaluate", - "tests/ctl/data/failing_dataset_field_taxonomy.yml", - ], - ) - print(result.output) - assert result.exit_code == 1 - - -@pytest.mark.integration -@pytest.mark.parametrize( - "export_resource", ["system", "dataset", "organization", "datamap"] -) -def test_export_resources( - test_config_path: str, - test_cli_runner: CliRunner, - export_resource: str, -) -> None: - """ - Tests that each resource is successfully exported - """ - - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "export", - export_resource, - "--dry", - ], - ) - assert result.exit_code == 0 - - -@pytest.mark.integration -def test_nested_field_fails_evaluation( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - """ - Tests a taxonomy that is rigged to fail only due to - one of the nested fields violating the policy. Test - will fail if the nested field is not discovered. - """ - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "evaluate", - "tests/ctl/data/failing_nested_dataset.yml", - ], - ) - print(result.output) - assert result.exit_code == 1 - - -@pytest.mark.integration -def test_generate_dataset_db_with_connection_string( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("dataset.yml") - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "dataset", - "db", - f"{tmp_file}", - "--connection-string", - "postgresql+psycopg2://postgres:fides@fides-db:5432/fides_test", - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.integration -def test_generate_dataset_db_with_credentials_id( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("dataset.yml") - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "dataset", - "db", - f"{tmp_file}", - "--credentials-id", - "postgres_1", - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.integration -def test_scan_dataset_db_input_connection_string( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "dataset", - "db", - "--connection-string", - "postgresql+psycopg2://postgres:fides@fides-db:5432/fides_test", - "--coverage-threshold", - "0", - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.integration -def test_scan_dataset_db_input_credentials_id( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "dataset", - "db", - "--credentials-id", - "postgres_1", - "--coverage-threshold", - "0", - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.external -def test_generate_system_aws_environment_credentials( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("system.yml") - result = test_cli_runner.invoke( - cli, - ["-f", test_config_path, "generate", "system", "aws", f"{tmp_file}"], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.external -def test_scan_system_aws_environment_credentials( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "system", - "aws", - "--coverage-threshold", - "0", - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.external -def test_generate_system_aws_input_credential_options( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("system.yml") - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "system", - "aws", - f"{tmp_file}", - "--access_key_id", - os.environ["AWS_ACCESS_KEY_ID"], - "--secret_access_key", - os.environ["AWS_SECRET_ACCESS_KEY"], - "--region", - os.environ["AWS_DEFAULT_REGION"], - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.external -def test_scan_system_aws_input_credential_options( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "system", - "aws", - "--coverage-threshold", - "0", - "--access_key_id", - os.environ["AWS_ACCESS_KEY_ID"], - "--secret_access_key", - os.environ["AWS_SECRET_ACCESS_KEY"], - "--region", - os.environ["AWS_DEFAULT_REGION"], - ], - ) - print(result.output) - assert result.exit_code == 0 - - -@pytest.mark.external -def test_generate_system_aws_input_credentials_id( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - os.environ["FIDES__CREDENTIALS__AWS_1__AWS_ACCESS_KEY_ID"] = os.environ[ - "AWS_ACCESS_KEY_ID" - ] - os.environ["FIDES__CREDENTIALS__AWS_1__AWS_SECRET_ACCESS_KEY"] = os.environ[ - "AWS_SECRET_ACCESS_KEY" - ] - tmp_file = tmpdir.join("system.yml") - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "system", - "aws", - f"{tmp_file}", - "--credentials-id", - "aws_1", - ], - ) - print(result.output) - assert result.exit_code == 0 - + @pytest.mark.integration + def test_scan_dataset_db_input_credentials_id( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "dataset", + "db", + "--credentials-id", + "postgres_1", + "--coverage-threshold", + "0", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_scan_system_aws_input_credentials_id( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - os.environ["FIDES__CREDENTIALS__AWS_1__AWS_ACCESS_KEY_ID"] = os.environ[ - "AWS_ACCESS_KEY_ID" - ] - os.environ["FIDES__CREDENTIALS__AWS_1__AWS_SECRET_ACCESS_KEY"] = os.environ[ - "AWS_SECRET_ACCESS_KEY" - ] + @pytest.mark.external + def test_scan_system_aws_environment_credentials( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "system", + "aws", + "--coverage-threshold", + "0", + ], + ) + print(result.output) + assert result.exit_code == 0 - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "system", - "aws", - "--coverage-threshold", - "0", - "--credentials-id", - "aws_1", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.external + def test_scan_system_aws_input_credential_options( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "system", + "aws", + "--coverage-threshold", + "0", + "--access_key_id", + os.environ["AWS_ACCESS_KEY_ID"], + "--secret_access_key", + os.environ["AWS_SECRET_ACCESS_KEY"], + "--region", + os.environ["AWS_DEFAULT_REGION"], + ], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.external + def test_scan_system_aws_input_credentials_id( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + os.environ["FIDES__CREDENTIALS__AWS_1__AWS_ACCESS_KEY_ID"] = os.environ[ + "AWS_ACCESS_KEY_ID" + ] + os.environ["FIDES__CREDENTIALS__AWS_1__AWS_SECRET_ACCESS_KEY"] = os.environ[ + "AWS_SECRET_ACCESS_KEY" + ] -@pytest.mark.external -def test_generate_system_okta_input_credential_options( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("system.yml") - token = os.environ["OKTA_CLIENT_TOKEN"] - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "system", - "okta", - f"{tmp_file}", - "--org-url", - OKTA_URL, - "--token", - token, - ], - ) - print(result.output) - assert result.exit_code == 0 + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "system", + "aws", + "--coverage-threshold", + "0", + "--credentials-id", + "aws_1", + ], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.external + def test_scan_system_okta_input_credential_options( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + token = os.environ["OKTA_CLIENT_TOKEN"] + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "system", + "okta", + "--org-url", + OKTA_URL, + "--token", + token, + "--coverage-threshold", + "0", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_scan_system_okta_input_credential_options( - test_config_path: str, test_cli_runner: CliRunner -) -> None: - token = os.environ["OKTA_CLIENT_TOKEN"] - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "system", - "okta", - "--org-url", - OKTA_URL, - "--token", - token, - "--coverage-threshold", - "0", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.external + def test_scan_system_okta_input_credentials_id( + self, + test_config_path: str, + test_cli_runner: CliRunner, + ) -> None: + os.environ["FIDES__CREDENTIALS__OKTA_1__TOKEN"] = os.environ[ + "OKTA_CLIENT_TOKEN" + ] + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "system", + "okta", + "--credentials-id", + "okta_1", + "--coverage-threshold", + "0", + ], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.external + def test_scan_system_okta_environment_credentials( + self, + test_config_path: str, + test_cli_runner: CliRunner, + ) -> None: + os.environ["OKTA_CLIENT_ORGURL"] = OKTA_URL + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "scan", + "system", + "okta", + "--coverage-threshold", + "0", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_generate_system_okta_environment_credentials( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("system.yml") - os.environ["OKTA_CLIENT_ORGURL"] = OKTA_URL - result = test_cli_runner.invoke( - cli, - ["-f", test_config_path, "generate", "system", "okta", f"{tmp_file}"], - ) - print(result.output) - assert result.exit_code == 0 +class TestGenerate: + @pytest.mark.integration + def test_generate_dataset_db_with_connection_string( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("dataset.yml") + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "dataset", + "db", + f"{tmp_file}", + "--connection-string", + "postgresql+psycopg2://postgres:fides@fides-db:5432/fides_test", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_scan_system_okta_environment_credentials( - test_config_path: str, - test_cli_runner: CliRunner, -) -> None: - os.environ["OKTA_CLIENT_ORGURL"] = OKTA_URL - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "system", - "okta", - "--coverage-threshold", - "0", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.integration + def test_generate_dataset_db_with_credentials_id( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("dataset.yml") + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "dataset", + "db", + f"{tmp_file}", + "--credentials-id", + "postgres_1", + ], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.external + def test_generate_system_aws_input_credential_options( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("system.yml") + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "system", + "aws", + f"{tmp_file}", + "--access_key_id", + os.environ["AWS_ACCESS_KEY_ID"], + "--secret_access_key", + os.environ["AWS_SECRET_ACCESS_KEY"], + "--region", + os.environ["AWS_DEFAULT_REGION"], + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_generate_system_okta_input_credentials_id( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: - tmp_file = tmpdir.join("system.yml") - os.environ["FIDES__CREDENTIALS__OKTA_1__TOKEN"] = os.environ["OKTA_CLIENT_TOKEN"] - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "system", - "okta", - f"{tmp_file}", - "--credentials-id", - "okta_1", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.external + def test_generate_system_aws_environment_credentials( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("system.yml") + result = test_cli_runner.invoke( + cli, + ["-f", test_config_path, "generate", "system", "aws", f"{tmp_file}"], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.external + def test_generate_system_aws_input_credentials_id( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + os.environ["FIDES__CREDENTIALS__AWS_1__AWS_ACCESS_KEY_ID"] = os.environ[ + "AWS_ACCESS_KEY_ID" + ] + os.environ["FIDES__CREDENTIALS__AWS_1__AWS_SECRET_ACCESS_KEY"] = os.environ[ + "AWS_SECRET_ACCESS_KEY" + ] + tmp_file = tmpdir.join("system.yml") + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "system", + "aws", + f"{tmp_file}", + "--credentials-id", + "aws_1", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_scan_system_okta_input_credentials_id( - test_config_path: str, - test_cli_runner: CliRunner, -) -> None: - os.environ["FIDES__CREDENTIALS__OKTA_1__TOKEN"] = os.environ["OKTA_CLIENT_TOKEN"] - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "scan", - "system", - "okta", - "--credentials-id", - "okta_1", - "--coverage-threshold", - "0", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.external + def test_generate_system_okta_input_credential_options( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("system.yml") + token = os.environ["OKTA_CLIENT_TOKEN"] + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "system", + "okta", + f"{tmp_file}", + "--org-url", + OKTA_URL, + "--token", + token, + ], + ) + print(result.output) + assert result.exit_code == 0 + @pytest.mark.external + def test_generate_system_okta_environment_credentials( + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("system.yml") + os.environ["OKTA_CLIENT_ORGURL"] = OKTA_URL + result = test_cli_runner.invoke( + cli, + ["-f", test_config_path, "generate", "system", "okta", f"{tmp_file}"], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_generate_dataset_bigquery_credentials_id( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: + @pytest.mark.external + def test_generate_system_okta_input_credentials_id( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_file = tmpdir.join("system.yml") + os.environ["FIDES__CREDENTIALS__OKTA_1__TOKEN"] = os.environ[ + "OKTA_CLIENT_TOKEN" + ] + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "system", + "okta", + f"{tmp_file}", + "--credentials-id", + "okta_1", + ], + ) + print(result.output) + assert result.exit_code == 0 - tmp_output_file = tmpdir.join("dataset.yml") - config_data = os.getenv("BIGQUERY_CONFIG", "e30=") - config_data_decoded = loads(b64decode(config_data.encode("utf-8")).decode("utf-8")) - os.environ["FIDES__CREDENTIALS__BIGQUERY_1__PROJECT_ID"] = config_data_decoded[ - "project_id" - ] - os.environ["FIDES__CREDENTIALS__BIGQUERY_1__PRIVATE_KEY_ID"] = config_data_decoded[ - "private_key_id" - ] - os.environ["FIDES__CREDENTIALS__BIGQUERY_1__PRIVATE_KEY"] = config_data_decoded[ - "private_key" - ] - os.environ["FIDES__CREDENTIALS__BIGQUERY_1__CLIENT_EMAIL"] = config_data_decoded[ - "client_email" - ] - os.environ["FIDES__CREDENTIALS__BIGQUERY_1__CLIENT_ID"] = config_data_decoded[ - "client_id" - ] - os.environ[ - "FIDES__CREDENTIALS__BIGQUERY_1__CLIENT_X509_CERT_URL" - ] = config_data_decoded["client_x509_cert_url"] - dataset_name = "fidesopstest" - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "dataset", - "gcp", - "bigquery", - dataset_name, - f"{tmp_output_file}", - "--credentials-id", - "bigquery_1", - ], - ) - print(result.output) - assert result.exit_code == 0 + @pytest.mark.external + def test_generate_dataset_bigquery_credentials_id( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: + tmp_output_file = tmpdir.join("dataset.yml") + config_data = os.getenv("BIGQUERY_CONFIG", "e30=") + config_data_decoded = loads( + b64decode(config_data.encode("utf-8")).decode("utf-8") + ) + os.environ["FIDES__CREDENTIALS__BIGQUERY_1__PROJECT_ID"] = config_data_decoded[ + "project_id" + ] + os.environ[ + "FIDES__CREDENTIALS__BIGQUERY_1__PRIVATE_KEY_ID" + ] = config_data_decoded["private_key_id"] + os.environ["FIDES__CREDENTIALS__BIGQUERY_1__PRIVATE_KEY"] = config_data_decoded[ + "private_key" + ] + os.environ[ + "FIDES__CREDENTIALS__BIGQUERY_1__CLIENT_EMAIL" + ] = config_data_decoded["client_email"] + os.environ["FIDES__CREDENTIALS__BIGQUERY_1__CLIENT_ID"] = config_data_decoded[ + "client_id" + ] + os.environ[ + "FIDES__CREDENTIALS__BIGQUERY_1__CLIENT_X509_CERT_URL" + ] = config_data_decoded["client_x509_cert_url"] + dataset_name = "fidesopstest" + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "dataset", + "gcp", + "bigquery", + dataset_name, + f"{tmp_output_file}", + "--credentials-id", + "bigquery_1", + ], + ) + print(result.output) + assert result.exit_code == 0 -@pytest.mark.external -def test_generate_dataset_bigquery_keyfile_path( - test_config_path: str, - test_cli_runner: CliRunner, - tmpdir: LocalPath, -) -> None: + @pytest.mark.external + def test_generate_dataset_bigquery_keyfile_path( + self, + test_config_path: str, + test_cli_runner: CliRunner, + tmpdir: LocalPath, + ) -> None: - tmp_output_file = tmpdir.join("dataset.yml") - tmp_keyfile = tmpdir.join("bigquery.json") - config_data = os.getenv("BIGQUERY_CONFIG", "e30=") - config_data_decoded = loads(b64decode(config_data.encode("utf-8")).decode("utf-8")) - with open(tmp_keyfile, "w", encoding="utf-8") as keyfile: - dump(config_data_decoded, keyfile) - dataset_name = "fidesopstest" - result = test_cli_runner.invoke( - cli, - [ - "-f", - test_config_path, - "generate", - "dataset", - "gcp", - "bigquery", - dataset_name, - f"{tmp_output_file}", - "--keyfile-path", - f"{tmp_keyfile}", - ], - ) - print(result.output) - assert result.exit_code == 0 + tmp_output_file = tmpdir.join("dataset.yml") + tmp_keyfile = tmpdir.join("bigquery.json") + config_data = os.getenv("BIGQUERY_CONFIG", "e30=") + config_data_decoded = loads( + b64decode(config_data.encode("utf-8")).decode("utf-8") + ) + with open(tmp_keyfile, "w", encoding="utf-8") as keyfile: + dump(config_data_decoded, keyfile) + dataset_name = "fidesopstest" + result = test_cli_runner.invoke( + cli, + [ + "-f", + test_config_path, + "generate", + "dataset", + "gcp", + "bigquery", + dataset_name, + f"{tmp_output_file}", + "--keyfile-path", + f"{tmp_keyfile}", + ], + ) + print(result.output) + assert result.exit_code == 0 From 07db01f0972ee91c84c3a78c81b6fee76e7d0c08 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 15:44:07 +0800 Subject: [PATCH 14/33] feat: add tests for the user CLI commands, known not passing --- tests/ctl/cli/test_cli.py | 44 ++++++++++++++++++++++++++++++++++++++ tests/ctl/test_config.toml | 2 ++ 2 files changed, 46 insertions(+) diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index e0bc360f37..baa0a30a42 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -843,3 +843,47 @@ def test_generate_dataset_bigquery_keyfile_path( ) print(result.output) assert result.exit_code == 0 + + +class TestUser: + """ + Test the "user" command group. + + Most tests rely on previous tests. + """ + + @pytest.mark.unit + def test_user_login_provide_credentials( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + """Test logging in as a user with a provided username and password.""" + result = test_cli_runner.invoke(cli, ["user", "login", "-u", "root_user", "-p", "Testpassword1!"]) + print(result.output) + assert result.exit_code == 0 + + @pytest.mark.unit + def test_user_login_credentials_file( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + """Test user login with an existing credentials file.""" + result = test_cli_runner.invoke(cli, ["user", "login"]) + print(result.output) + assert result.exit_code == 0 + + @pytest.mark.unit + def test_user_create( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + """Test creating a user with the current credentials.""" + result = test_cli_runner.invoke(cli, ["user", "create"]) + print(result.output) + assert result.exit_code == 0 + + @pytest.mark.unit + def test_user_permissions( + self, test_config_path: str, test_cli_runner: CliRunner + ) -> None: + """Test getting user permissions for the current user.""" + result = test_cli_runner.invoke(cli, ["user", "permissions"]) + print(result.output) + assert result.exit_code == 0 diff --git a/tests/ctl/test_config.toml b/tests/ctl/test_config.toml index 8525aad788..65c74eca28 100644 --- a/tests/ctl/test_config.toml +++ b/tests/ctl/test_config.toml @@ -44,6 +44,8 @@ client_x509_cert_url = "redacted_url_override_in_tests" [security] app_encryption_key = "OLMkv91j8DHiDAULnK5Lxx3kSCov30b3" +root_username = "root_user" +root_password = "Testpassword1!" oauth_root_client_id = "fidesadmin" oauth_root_client_secret = "fidesadminsecret" drp_jwt_secret = "secret" From 02a0af384e17f1bcd3ce2fefcfb89166932b4271 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 9 Jan 2023 15:51:46 +0800 Subject: [PATCH 15/33] feat: add the first test for the new user module --- tests/ctl/core/test_user.py | 15 +++++++++++++++ tests/ctl/core/test_users.py | 3 --- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 tests/ctl/core/test_user.py delete mode 100644 tests/ctl/core/test_users.py diff --git a/tests/ctl/core/test_user.py b/tests/ctl/core/test_user.py new file mode 100644 index 0000000000..9f4f1f0f30 --- /dev/null +++ b/tests/ctl/core/test_user.py @@ -0,0 +1,15 @@ +import pytest + +from fides.core.user import Credentials + +@pytest.mark.unit +class TestCredentials: + """ + Test the Credentials object. + """ + def test_valid_credentials(self): + credentials = Credentials(username="test", password="password", user_id="some_id",access_token="some_token") + assert credentials.username == "test" + assert credentials.password == "password" + assert credentials.user_id == "some_id" + assert credentials.access_token == "some_token" diff --git a/tests/ctl/core/test_users.py b/tests/ctl/core/test_users.py deleted file mode 100644 index 739f349bad..0000000000 --- a/tests/ctl/core/test_users.py +++ /dev/null @@ -1,3 +0,0 @@ -import pytest - -# Test each function From 8b9f973f334ef20491c66fc90caeba3d0d21fc5c Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 10:40:21 +0800 Subject: [PATCH 16/33] fix: static checks --- src/fides/cli/__init__.py | 2 +- src/fides/cli/commands/user.py | 9 +++++---- src/fides/core/user.py | 4 ++-- tests/ctl/cli/test_cli.py | 4 +++- tests/ctl/core/test_user.py | 9 ++++++++- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/fides/cli/__init__.py b/src/fides/cli/__init__.py index bf6f4edc58..4a383e8af8 100644 --- a/src/fides/cli/__init__.py +++ b/src/fides/cli/__init__.py @@ -17,9 +17,9 @@ from .commands.export import export from .commands.generate import generate from .commands.scan import scan +from .commands.user import user 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] diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 8114bde721..e59bd8ea8d 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -1,17 +1,18 @@ """Contains the user group of commands for fides.""" -import click from typing import List +import click + from fides.core.user import ( + CREDENTIALS_PATH, + create_auth_header, create_user, get_access_token, - write_credentials_file, - CREDENTIALS_PATH, get_user_permissions, - create_auth_header, read_credentials_file, update_user_permissions, + write_credentials_file, ) from fides.core.utils import echo_green, echo_red from fides.lib.oauth.scopes import SCOPES diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 63a6bf4be4..9eeb553863 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -1,11 +1,11 @@ """Module for interaction with User endpoints/commands.""" +import json from pathlib import Path from typing import Dict, List, Tuple -from pydantic import BaseModel -import json import requests import toml +from pydantic import BaseModel from fides.cli.utils import handle_cli_response diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index baa0a30a42..6d965b5154 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -857,7 +857,9 @@ def test_user_login_provide_credentials( self, test_config_path: str, test_cli_runner: CliRunner ) -> None: """Test logging in as a user with a provided username and password.""" - result = test_cli_runner.invoke(cli, ["user", "login", "-u", "root_user", "-p", "Testpassword1!"]) + result = test_cli_runner.invoke( + cli, ["user", "login", "-u", "root_user", "-p", "Testpassword1!"] + ) print(result.output) assert result.exit_code == 0 diff --git a/tests/ctl/core/test_user.py b/tests/ctl/core/test_user.py index 9f4f1f0f30..ff3b8097a5 100644 --- a/tests/ctl/core/test_user.py +++ b/tests/ctl/core/test_user.py @@ -2,13 +2,20 @@ from fides.core.user import Credentials + @pytest.mark.unit class TestCredentials: """ Test the Credentials object. """ + def test_valid_credentials(self): - credentials = Credentials(username="test", password="password", user_id="some_id",access_token="some_token") + credentials = Credentials( + username="test", + password="password", + user_id="some_id", + access_token="some_token", + ) assert credentials.username == "test" assert credentials.password == "password" assert credentials.user_id == "some_id" From 6799f9ae02f769436e3a6928368de107a11d7cbb Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 11:34:14 +0800 Subject: [PATCH 17/33] fix: add b64 encode to password --- src/fides/core/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 9eeb553863..da6529e5d9 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -2,6 +2,7 @@ import json from pathlib import Path from typing import Dict, List, Tuple +from fides.lib.cryptography.cryptographic_util import str_to_b64_str import requests import toml @@ -33,7 +34,7 @@ def get_access_token(username: str, password: str) -> Tuple[str, str]: """ payload = { "username": username, - "password": password, + "password": str_to_b64_str(password), } response = requests.post(LOGIN_URL, json=payload) @@ -85,7 +86,7 @@ def create_user( """Create a user.""" request_data = { "username": username, - "password": password, + "password": str_to_b64_str(password), "first_name": first_name, "last_name": last_name, } From 3bd1dfbdebc4549fb0d89eacd4a7ff8004e0a491 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 11:52:54 +0800 Subject: [PATCH 18/33] feat: print the newly logged-in user --- src/fides/cli/commands/user.py | 1 + src/fides/core/user.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index e59bd8ea8d..c11d3bfbe5 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -93,6 +93,7 @@ def login(username: str, password: str) -> None: password = credentials.password user_id, access_token = get_access_token(username, password) + echo_green(f"Logged in as user: {username}") write_credentials_file(username, password, user_id, access_token) echo_green(f"Credentials file written to: {CREDENTIALS_PATH}") diff --git a/src/fides/core/user.py b/src/fides/core/user.py index da6529e5d9..8128e7218d 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -2,13 +2,13 @@ import json from pathlib import Path from typing import Dict, List, Tuple -from fides.lib.cryptography.cryptographic_util import str_to_b64_str import requests import toml from pydantic import BaseModel from fides.cli.utils import handle_cli_response +from fides.lib.cryptography.cryptographic_util import str_to_b64_str CREATE_USER_URL = "http://localhost:8080/api/v1/user" LOGIN_URL = "http://localhost:8080/api/v1/login" From da58d27b61d8ace2a934ae0fc12ba5e950e32165 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 15:10:48 +0800 Subject: [PATCH 19/33] fix: pass in the server_url instead of hardcoding it --- src/fides/cli/commands/user.py | 47 ++++++++++++++++++++++++---------- src/fides/core/user.py | 36 +++++++++++++++----------- tests/ctl/cli/test_cli.py | 4 ++- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index c11d3bfbe5..7788d05fd8 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -5,7 +5,6 @@ import click from fides.core.user import ( - CREDENTIALS_PATH, create_auth_header, create_user, get_access_token, @@ -27,6 +26,7 @@ def user(ctx: click.Context) -> None: @user.command() +@click.pass_context @click.argument("username", type=str) @click.argument("password", type=str) @click.option( @@ -41,12 +41,16 @@ def user(ctx: click.Context) -> None: default="", help="Last name of the user.", ) -def create(username: str, password: str, first_name: str, last_name: str) -> None: +def create( + ctx: click.Context, username: str, password: str, first_name: str, last_name: str +) -> None: """ Use credentials from the credentials file to create a new user. Gives full permissions. """ + config = ctx.obj["CONFIG"] + server_url = config.cli.server_url try: credentials = read_credentials_file() @@ -56,13 +60,23 @@ def create(username: str, password: str, first_name: str, last_name: str) -> Non access_token = credentials.access_token auth_header = create_auth_header(access_token) - user_response = create_user(username, password, first_name, last_name, auth_header) + user_response = create_user( + username=username, + password=password, + first_name=first_name, + last_name=last_name, + auth_header=auth_header, + server_url=server_url, + ) user_id = user_response.json()["id"] - update_user_permissions(user_id=user_id, scopes=SCOPES, auth_header=auth_header) + update_user_permissions( + user_id=user_id, scopes=SCOPES, auth_header=auth_header, server_url=server_url + ) echo_green(f"User: '{username}' created and assigned permissions.") @user.command() +@click.pass_context @click.option( "-u", "--username", @@ -75,13 +89,15 @@ def create(username: str, password: str, first_name: str, last_name: str) -> Non default="", help="Password to authenticate with.", ) -def login(username: str, password: str) -> None: +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 is provided, attempt to load an existing credentials file and use that username/password. """ + config = ctx.obj["CONFIG"] + server_url = config.cli.server_url if not (username and password): try: @@ -92,22 +108,27 @@ def login(username: str, password: str) -> None: username = credentials.username password = credentials.password - user_id, access_token = get_access_token(username, password) + user_id, access_token = get_access_token( + username=username, password=password, server_url=server_url + ) echo_green(f"Logged in as user: {username}") - write_credentials_file(username, password, user_id, access_token) - echo_green(f"Credentials file written to: {CREDENTIALS_PATH}") + credentials_path = write_credentials_file(username, password, user_id, access_token) + echo_green(f"Credentials file written to: {credentials_path}") @user.command() -def permissions() -> None: +@click.pass_context +def permissions(ctx: click.Context) -> None: """List the scopes avaible to the current user.""" + config = ctx.obj["CONFIG"] + server_url = config.cli.server_url credentials = read_credentials_file() user_id = credentials.user_id access_token = credentials.access_token auth_header = create_auth_header(access_token) - scopes: List[str] = get_user_permissions(user_id, auth_header) - print("Scopes:") - for scope in scopes: - print(scope) + permissions: List[str] = get_user_permissions(user_id, auth_header, server_url) + print("Permissions:") + for permission in permissions: + print(f"\t{permission}") diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 8128e7218d..8f88409182 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -9,12 +9,14 @@ from fides.cli.utils import handle_cli_response from fides.lib.cryptography.cryptographic_util import str_to_b64_str +from fides.core.config import get_config -CREATE_USER_URL = "http://localhost:8080/api/v1/user" -LOGIN_URL = "http://localhost:8080/api/v1/login" -USER_PERMISSIONS_URL = "http://localhost:8080/api/v1/user/{}/permission" +config = get_config() +CREATE_USER_PATH = "/api/v1/user" +LOGIN_PATH = "/api/v1/login" +USER_PERMISSIONS_PATH = "/api/v1/user/{}/permission" -CREDENTIALS_PATH = f"{str(Path.home())}/.fides_credentials" +CREDENTIALS_FILE_PATH = f"{str(Path.home())}/.fides_credentials" class Credentials(BaseModel): @@ -28,7 +30,7 @@ class Credentials(BaseModel): access_token: str -def get_access_token(username: str, password: str) -> Tuple[str, str]: +def get_access_token(username: str, password: str, server_url: str) -> Tuple[str, str]: """ Get a user access token from the webserver. """ @@ -37,7 +39,7 @@ def get_access_token(username: str, password: str) -> Tuple[str, str]: "password": str_to_b64_str(password), } - response = requests.post(LOGIN_URL, json=payload) + response = requests.post(server_url + LOGIN_PATH, json=payload) handle_cli_response(response, verbose=False) user_id: str = response.json()["user_data"]["id"] access_token: str = response.json()["token_data"]["access_token"] @@ -56,14 +58,14 @@ def write_credentials_file( user_id=user_id, access_token=access_token, ) - with open(CREDENTIALS_PATH, "w", encoding="utf-8") as credentials_file: + with open(CREDENTIALS_FILE_PATH, "w", encoding="utf-8") as credentials_file: credentials_file.write(toml.dumps(credentials.dict())) - return CREDENTIALS_PATH + return CREDENTIALS_FILE_PATH def read_credentials_file() -> Credentials: """Read and return the credentials file.""" - with open(CREDENTIALS_PATH, "r", encoding="utf-8") as credentials_file: + with open(CREDENTIALS_FILE_PATH, "r", encoding="utf-8") as credentials_file: credentials = Credentials.parse_obj(toml.load(credentials_file)) return credentials @@ -82,6 +84,7 @@ def create_user( first_name: str, last_name: str, auth_header: Dict[str, str], + server_url: str, ) -> requests.Response: """Create a user.""" request_data = { @@ -91,7 +94,7 @@ def create_user( "last_name": last_name, } response = requests.post( - CREATE_USER_URL, + server_url + CREATE_USER_PATH, headers=auth_header, data=json.dumps(request_data), ) @@ -99,13 +102,15 @@ def create_user( return response -def get_user_permissions(user_id: str, auth_header: Dict[str, str]) -> List[str]: +def get_user_permissions( + user_id: str, auth_header: Dict[str, str], server_url: str +) -> List[str]: """ List all of the user permissions for the provided user. """ - scopes_url = USER_PERMISSIONS_URL.format(user_id) + get_permissions_path = USER_PERMISSIONS_PATH.format(user_id) response = requests.get( - scopes_url, + server_url + get_permissions_path, headers=auth_header, ) @@ -114,14 +119,15 @@ def get_user_permissions(user_id: str, auth_header: Dict[str, str]) -> List[str] def update_user_permissions( - user_id: str, scopes: List[str], auth_header: Dict[str, str] + user_id: str, scopes: List[str], auth_header: Dict[str, str], server_url: str ) -> requests.Response: """ Update user permissions for a given user. """ request_data = {"scopes": scopes, "id": user_id} + set_permissions_path = USER_PERMISSIONS_PATH.format(user_id) response = requests.put( - USER_PERMISSIONS_URL.format(user_id), + server_url + set_permissions_path, headers=auth_header, json=request_data, ) diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index 6d965b5154..d53940e4f3 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -877,7 +877,9 @@ def test_user_create( self, test_config_path: str, test_cli_runner: CliRunner ) -> None: """Test creating a user with the current credentials.""" - result = test_cli_runner.invoke(cli, ["user", "create"]) + result = test_cli_runner.invoke( + cli, ["user", "create", "newuser", "Newpassword1!"] + ) print(result.output) assert result.exit_code == 0 From 3ea2e864c6a6a46d2bd98c4773fc11094232fdcc Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 15:39:03 +0800 Subject: [PATCH 20/33] fix: cli user tests --- tests/ctl/cli/test_cli.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index d53940e4f3..6d2d8d3f49 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -858,7 +858,17 @@ def test_user_login_provide_credentials( ) -> None: """Test logging in as a user with a provided username and password.""" result = test_cli_runner.invoke( - cli, ["user", "login", "-u", "root_user", "-p", "Testpassword1!"] + cli, + [ + "-f", + test_config_path, + "user", + "login", + "-u", + "root_user", + "-p", + "Testpassword1!", + ], ) print(result.output) assert result.exit_code == 0 @@ -868,7 +878,7 @@ def test_user_login_credentials_file( self, test_config_path: str, test_cli_runner: CliRunner ) -> None: """Test user login with an existing credentials file.""" - result = test_cli_runner.invoke(cli, ["user", "login"]) + result = test_cli_runner.invoke(cli, ["-f", test_config_path, "user", "login"]) print(result.output) assert result.exit_code == 0 @@ -878,7 +888,7 @@ def test_user_create( ) -> None: """Test creating a user with the current credentials.""" result = test_cli_runner.invoke( - cli, ["user", "create", "newuser", "Newpassword1!"] + cli, ["-f", test_config_path, "user", "create", "newuser", "Newpassword1!"] ) print(result.output) assert result.exit_code == 0 @@ -888,6 +898,8 @@ def test_user_permissions( self, test_config_path: str, test_cli_runner: CliRunner ) -> None: """Test getting user permissions for the current user.""" - result = test_cli_runner.invoke(cli, ["user", "permissions"]) + result = test_cli_runner.invoke( + cli, ["-f", test_config_path, "user", "permissions"] + ) print(result.output) assert result.exit_code == 0 From 7429c81e5806c1a20afed266c9db15af542cdf7e Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 15:53:25 +0800 Subject: [PATCH 21/33] fix: static checks --- src/fides/cli/commands/user.py | 5 +++-- src/fides/core/user.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 7788d05fd8..c66b42d997 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -116,9 +116,9 @@ def login(ctx: click.Context, username: str, password: str) -> None: echo_green(f"Credentials file written to: {credentials_path}") -@user.command() +@user.command(name="permissions") @click.pass_context -def permissions(ctx: click.Context) -> None: +def get_permissions(ctx: click.Context) -> None: """List the scopes avaible to the current user.""" config = ctx.obj["CONFIG"] server_url = config.cli.server_url @@ -129,6 +129,7 @@ def permissions(ctx: click.Context) -> None: auth_header = create_auth_header(access_token) permissions: List[str] = get_user_permissions(user_id, auth_header, server_url) + print("Permissions:") for permission in permissions: print(f"\t{permission}") diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 8f88409182..228f9ec740 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -8,8 +8,8 @@ from pydantic import BaseModel from fides.cli.utils import handle_cli_response -from fides.lib.cryptography.cryptographic_util import str_to_b64_str from fides.core.config import get_config +from fides.lib.cryptography.cryptographic_util import str_to_b64_str config = get_config() CREATE_USER_PATH = "/api/v1/user" From 436fc904186823072f1624adbfcadb5319ae56df Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 12:19:58 +0800 Subject: [PATCH 22/33] feat: prompt the user for username/password if not provided and obscure the password input --- tests/ctl/cli/test_cli.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index 6d2d8d3f49..1312d34688 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -873,15 +873,6 @@ def test_user_login_provide_credentials( print(result.output) assert result.exit_code == 0 - @pytest.mark.unit - def test_user_login_credentials_file( - self, test_config_path: str, test_cli_runner: CliRunner - ) -> None: - """Test user login with an existing credentials file.""" - result = test_cli_runner.invoke(cli, ["-f", test_config_path, "user", "login"]) - print(result.output) - assert result.exit_code == 0 - @pytest.mark.unit def test_user_create( self, test_config_path: str, test_cli_runner: CliRunner From 661d6a54ea5b4148b9cb2cb4afb2df9c5b93aaa8 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 12:20:12 +0800 Subject: [PATCH 23/33] fix: add the functionality from the last commit --- src/fides/cli/commands/user.py | 48 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index c66b42d997..7844a4b898 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -25,21 +25,43 @@ def user(ctx: click.Context) -> None: """ +def prompt_username(ctx: click.Context, param: str, value: str) -> str: + if not value: + value = click.prompt(text="Username") + return value + + +def prompt_password(ctx: click.Context, param: str, value: str) -> str: + if not value: + value = click.prompt(text="Password", hide_input=True) + return value + + @user.command() @click.pass_context -@click.argument("username", type=str) -@click.argument("password", type=str) +@click.option( + "-u", + "--username", + default="", + callback=prompt_username, +) +@click.option( + "-p", + "--password", + default="", + callback=prompt_password, +) @click.option( "-f", "--first-name", default="", - help="First name of the user.", + help="First name of the new user.", ) @click.option( "-l", "--last-name", default="", - help="Last name of the user.", + help="Last name of the new user.", ) def create( ctx: click.Context, username: str, password: str, first_name: str, last_name: str @@ -81,33 +103,21 @@ def create( "-u", "--username", default="", - help="Username to authenticate with.", + callback=prompt_username, ) @click.option( "-p", "--password", default="", - help="Password to authenticate with.", + callback=prompt_password, ) 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 is provided, attempt to load an existing credentials file - and use that username/password. + Use credentials to get a user access token and write it to a credentials file. """ config = ctx.obj["CONFIG"] server_url = config.cli.server_url - if not (username and password): - try: - credentials = read_credentials_file() - except FileNotFoundError: - echo_red("No username/password provided and no credentials file found.") - raise SystemExit(1) - username = credentials.username - password = credentials.password - user_id, access_token = get_access_token( username=username, password=password, server_url=server_url ) From 0ea7672a91cf72fb238eec98859ffd86dc0f533c Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 12:25:36 +0800 Subject: [PATCH 24/33] refactor: move custom user options into options.py --- src/fides/cli/commands/user.py | 55 +++++----------------------------- src/fides/cli/options.py | 50 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 7844a4b898..46de771637 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -15,6 +15,7 @@ ) from fides.core.utils import echo_green, echo_red from fides.lib.oauth.scopes import SCOPES +from fides.cli.options import username, password, first_name, last_name @click.group(name="user") @@ -25,44 +26,12 @@ def user(ctx: click.Context) -> None: """ -def prompt_username(ctx: click.Context, param: str, value: str) -> str: - if not value: - value = click.prompt(text="Username") - return value - - -def prompt_password(ctx: click.Context, param: str, value: str) -> str: - if not value: - value = click.prompt(text="Password", hide_input=True) - return value - - @user.command() @click.pass_context -@click.option( - "-u", - "--username", - default="", - callback=prompt_username, -) -@click.option( - "-p", - "--password", - default="", - callback=prompt_password, -) -@click.option( - "-f", - "--first-name", - default="", - help="First name of the new user.", -) -@click.option( - "-l", - "--last-name", - default="", - help="Last name of the new user.", -) +@username +@password +@first_name +@last_name def create( ctx: click.Context, username: str, password: str, first_name: str, last_name: str ) -> None: @@ -99,18 +68,8 @@ def create( @user.command() @click.pass_context -@click.option( - "-u", - "--username", - default="", - callback=prompt_username, -) -@click.option( - "-p", - "--password", - default="", - callback=prompt_password, -) +@username +@password def login(ctx: click.Context, username: str, password: str) -> None: """ Use credentials to get a user access token and write it to a credentials file. diff --git a/src/fides/cli/options.py b/src/fides/cli/options.py index 7c28c4ff85..8e5ac01b63 100644 --- a/src/fides/cli/options.py +++ b/src/fides/cli/options.py @@ -185,3 +185,53 @@ def aws_region_option(command: Callable) -> Callable: help="Use region option to connect to aws. Requires options --access_key_id, --secret_access_key and --region", )(command) return command + + +def prompt_username(ctx: click.Context, param: str, value: str) -> str: + if not value: + value = click.prompt(text="Username") + return value + + +def prompt_password(ctx: click.Context, param: str, value: str) -> str: + if not value: + value = click.prompt(text="Password", hide_input=True) + return value + + +def username(command: Callable) -> Callable: + command = click.option( + "-u", + "--username", + default="", + callback=prompt_username, + )(command) + return command + + +def password(command: Callable) -> Callable: + command = click.option( + "-p", + "--password", + default="", + callback=prompt_password, + )(command) + return command + + +def first_name(command: Callable) -> Callable: + command = click.option( + "-f", + "--first-name", + default="", + )(command) + return command + + +def last_name(command: Callable) -> Callable: + command = click.option( + "-l", + "--last-name", + default="", + )(command) + return command From eb48830827d7c2f3d8f9d6af69161298480ec986 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 13:25:44 +0800 Subject: [PATCH 25/33] refactor: write_credentials_file takes a Credentials object --- src/fides/cli/commands/user.py | 8 ++++++-- src/fides/core/user.py | 10 +--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 46de771637..eded3492d9 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -12,6 +12,7 @@ read_credentials_file, update_user_permissions, write_credentials_file, + Credentials, ) from fides.core.utils import echo_green, echo_red from fides.lib.oauth.scopes import SCOPES @@ -38,7 +39,7 @@ def create( """ Use credentials from the credentials file to create a new user. - Gives full permissions. + Gives full permissions to the new user. """ config = ctx.obj["CONFIG"] server_url = config.cli.server_url @@ -81,7 +82,10 @@ def login(ctx: click.Context, username: str, password: str) -> None: username=username, password=password, server_url=server_url ) echo_green(f"Logged in as user: {username}") - credentials_path = write_credentials_file(username, password, user_id, access_token) + credentials = Credentials( + username=username, password=password, user_id=user_id, access_token=access_token + ) + credentials_path = write_credentials_file(credentials) echo_green(f"Credentials file written to: {credentials_path}") diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 228f9ec740..30a9c57e8f 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -46,18 +46,10 @@ def get_access_token(username: str, password: str, server_url: str) -> Tuple[str return (user_id, access_token) -def write_credentials_file( - username: str, password: str, user_id: str, access_token: str -) -> str: +def write_credentials_file(credentials: Credentials) -> str: """ Write the user credentials file. """ - credentials = Credentials( - username=username, - password=password, - user_id=user_id, - access_token=access_token, - ) with open(CREDENTIALS_FILE_PATH, "w", encoding="utf-8") as credentials_file: credentials_file.write(toml.dumps(credentials.dict())) return CREDENTIALS_FILE_PATH From 57ad1a9f4e166aed0acd4d533023f2599a204d9b Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 15:28:41 +0800 Subject: [PATCH 26/33] fix: update user CLI tests to use a temp dir --- tests/ctl/cli/test_cli.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index 1312d34688..8d6040bf25 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -3,6 +3,7 @@ from base64 import b64decode from json import dump, loads from typing import Generator +from pathlib import Path import pytest from click.testing import CliRunner @@ -845,6 +846,13 @@ def test_generate_dataset_bigquery_keyfile_path( assert result.exit_code == 0 +@pytest.fixture(scope="class") +def credentials_path(tmp_path_factory) -> str: + credentials_dir = tmp_path_factory.mktemp("credentials") + credentials_path = credentials_dir / ".fides_credentials" + return str(credentials_path) + + class TestUser: """ Test the "user" command group. @@ -854,7 +862,7 @@ class TestUser: @pytest.mark.unit def test_user_login_provide_credentials( - self, test_config_path: str, test_cli_runner: CliRunner + self, test_config_path: str, test_cli_runner: CliRunner, credentials_path: str ) -> None: """Test logging in as a user with a provided username and password.""" result = test_cli_runner.invoke( @@ -869,28 +877,42 @@ def test_user_login_provide_credentials( "-p", "Testpassword1!", ], + env={"FIDES_CREDENTIALS_PATH": credentials_path}, ) print(result.output) assert result.exit_code == 0 @pytest.mark.unit def test_user_create( - self, test_config_path: str, test_cli_runner: CliRunner + self, test_config_path: str, test_cli_runner: CliRunner, credentials_path: str ) -> None: """Test creating a user with the current credentials.""" result = test_cli_runner.invoke( - cli, ["-f", test_config_path, "user", "create", "newuser", "Newpassword1!"] + cli, + [ + "-f", + test_config_path, + "user", + "create", + "-u", + "newuser", + "-p", + "Newpassword1!", + ], + env={"FIDES_CREDENTIALS_PATH": credentials_path}, ) print(result.output) assert result.exit_code == 0 @pytest.mark.unit def test_user_permissions( - self, test_config_path: str, test_cli_runner: CliRunner + self, test_config_path: str, test_cli_runner: CliRunner, credentials_path: str ) -> None: """Test getting user permissions for the current user.""" result = test_cli_runner.invoke( - cli, ["-f", test_config_path, "user", "permissions"] + cli, + ["-f", test_config_path, "user", "permissions"], + env={"FIDES_CREDENTIALS_PATH": credentials_path}, ) print(result.output) assert result.exit_code == 0 From 2fdcf6c721f9da93e229a089344379e226b243a4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 15:29:58 +0800 Subject: [PATCH 27/33] feat: check for credentials path in env var --- src/fides/core/user.py | 24 +++++++++++++++++------- tests/ctl/core/test_user.py | 11 ++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 30a9c57e8f..71a10c55a4 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -2,6 +2,7 @@ import json from pathlib import Path from typing import Dict, List, Tuple +from os import getenv import requests import toml @@ -16,8 +17,6 @@ LOGIN_PATH = "/api/v1/login" USER_PERMISSIONS_PATH = "/api/v1/user/{}/permission" -CREDENTIALS_FILE_PATH = f"{str(Path.home())}/.fides_credentials" - class Credentials(BaseModel): """ @@ -30,6 +29,13 @@ class Credentials(BaseModel): access_token: str +def get_credentials_path() -> str: + """Returns the default credentials path or the path set as an environment variable.""" + default_credentials_file_path = f"{str(Path.home())}/.fides_credentials" + credentials_path = getenv("FIDES_CREDENTIALS_PATH", default_credentials_file_path) + return credentials_path + + def get_access_token(username: str, password: str, server_url: str) -> Tuple[str, str]: """ Get a user access token from the webserver. @@ -46,18 +52,22 @@ def get_access_token(username: str, password: str, server_url: str) -> Tuple[str return (user_id, access_token) -def write_credentials_file(credentials: Credentials) -> str: +def write_credentials_file( + credentials: Credentials, credentials_path: str = get_credentials_path() +) -> str: """ Write the user credentials file. """ - with open(CREDENTIALS_FILE_PATH, "w", encoding="utf-8") as credentials_file: + with open(credentials_path, "w", encoding="utf-8") as credentials_file: credentials_file.write(toml.dumps(credentials.dict())) - return CREDENTIALS_FILE_PATH + return credentials_path -def read_credentials_file() -> Credentials: +def read_credentials_file( + credentials_path: str = get_credentials_path(), +) -> Credentials: """Read and return the credentials file.""" - with open(CREDENTIALS_FILE_PATH, "r", encoding="utf-8") as credentials_file: + with open(credentials_path, "r", encoding="utf-8") as credentials_file: credentials = Credentials.parse_obj(toml.load(credentials_file)) return credentials diff --git a/tests/ctl/core/test_user.py b/tests/ctl/core/test_user.py index ff3b8097a5..1cdca2025d 100644 --- a/tests/ctl/core/test_user.py +++ b/tests/ctl/core/test_user.py @@ -1,6 +1,7 @@ import pytest +from os import environ -from fides.core.user import Credentials +from fides.core.user import Credentials, get_credentials_path @pytest.mark.unit @@ -20,3 +21,11 @@ def test_valid_credentials(self): assert credentials.password == "password" assert credentials.user_id == "some_id" assert credentials.access_token == "some_token" + + +def test_get_credentials_path() -> None: + """Test that a custom path for the credentials file works as expected.""" + expected_path = "test_credentials" + environ["FIDES_CREDENTIALS_PATH"] = "test_credentials" + actual_path = get_credentials_path() + assert expected_path == actual_path From 33a8f086e245959f5b08b357e858384274dac6e3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 15:40:18 +0800 Subject: [PATCH 28/33] refactor: move all command logic from the CLI commands to standard functions in core/user.py --- src/fides/cli/commands/user.py | 53 ++++------------------------ src/fides/core/user.py | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index eded3492d9..1ae45449ca 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -5,17 +5,10 @@ import click from fides.core.user import ( - create_auth_header, - create_user, - get_access_token, - get_user_permissions, - read_credentials_file, - update_user_permissions, - write_credentials_file, - Credentials, + create_command, + login_command, + get_permissions_command, ) -from fides.core.utils import echo_green, echo_red -from fides.lib.oauth.scopes import SCOPES from fides.cli.options import username, password, first_name, last_name @@ -43,28 +36,13 @@ def create( """ config = ctx.obj["CONFIG"] server_url = config.cli.server_url - - try: - credentials = read_credentials_file() - except FileNotFoundError: - echo_red("No credentials file found.") - raise SystemExit(1) - - access_token = credentials.access_token - auth_header = create_auth_header(access_token) - user_response = create_user( + create_command( username=username, password=password, first_name=first_name, last_name=last_name, - auth_header=auth_header, server_url=server_url, ) - user_id = user_response.json()["id"] - update_user_permissions( - user_id=user_id, scopes=SCOPES, auth_header=auth_header, server_url=server_url - ) - echo_green(f"User: '{username}' created and assigned permissions.") @user.command() @@ -77,16 +55,7 @@ def login(ctx: click.Context, username: str, password: str) -> None: """ config = ctx.obj["CONFIG"] server_url = config.cli.server_url - - user_id, access_token = get_access_token( - username=username, password=password, server_url=server_url - ) - echo_green(f"Logged in as user: {username}") - credentials = Credentials( - username=username, password=password, user_id=user_id, access_token=access_token - ) - credentials_path = write_credentials_file(credentials) - echo_green(f"Credentials file written to: {credentials_path}") + login_command(username=username, password=password, server_url=server_url) @user.command(name="permissions") @@ -95,14 +64,4 @@ def get_permissions(ctx: click.Context) -> None: """List the scopes avaible to the current user.""" config = ctx.obj["CONFIG"] server_url = config.cli.server_url - - credentials = read_credentials_file() - user_id = credentials.user_id - access_token = credentials.access_token - - auth_header = create_auth_header(access_token) - permissions: List[str] = get_user_permissions(user_id, auth_header, server_url) - - print("Permissions:") - for permission in permissions: - print(f"\t{permission}") + get_permissions_command(server_url=server_url) diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 71a10c55a4..b69361f04e 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -11,6 +11,8 @@ from fides.cli.utils import handle_cli_response from fides.core.config import get_config from fides.lib.cryptography.cryptographic_util import str_to_b64_str +from fides.core.utils import echo_green, echo_red +from fides.lib.oauth.scopes import SCOPES config = get_config() CREATE_USER_PATH = "/api/v1/user" @@ -135,3 +137,64 @@ def update_user_permissions( ) handle_cli_response(response, verbose=False) return response + + +def create_command( + username: str, password: str, first_name: str, last_name: str, server_url: str +) -> None: + """ + Logic for creating a user from the CLI. + """ + + try: + credentials = read_credentials_file() + except FileNotFoundError: + echo_red("No credentials file found.") + raise SystemExit(1) + + access_token = credentials.access_token + auth_header = create_auth_header(access_token) + user_response = create_user( + username=username, + password=password, + first_name=first_name, + last_name=last_name, + auth_header=auth_header, + server_url=server_url, + ) + user_id = user_response.json()["id"] + update_user_permissions( + user_id=user_id, scopes=SCOPES, auth_header=auth_header, server_url=server_url + ) + echo_green(f"User: '{username}' created and assigned permissions.") + + +def login_command(username: str, password: str, server_url: str) -> None: + """ + Logic for logging in from the CLI. + """ + user_id, access_token = get_access_token( + username=username, password=password, server_url=server_url + ) + echo_green(f"Logged in as user: {username}") + credentials = Credentials( + username=username, password=password, user_id=user_id, access_token=access_token + ) + credentials_path = write_credentials_file(credentials) + echo_green(f"Credentials file written to: {credentials_path}") + + +def get_permissions_command(server_url: str) -> None: + """ + Logic for getting user permissions from the CLI. + """ + credentials = read_credentials_file() + user_id = credentials.user_id + access_token = credentials.access_token + + auth_header = create_auth_header(access_token) + permissions: List[str] = get_user_permissions(user_id, auth_header, server_url) + + print("Permissions:") + for permission in permissions: + print(f"\t{permission}") From 14125f5ca2ca2d090bef73a09d6d018500fb1f2b Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 15:42:59 +0800 Subject: [PATCH 29/33] docs: update user command docstrings --- docker-compose.yml | 2 +- src/fides/core/user.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1f519f9878..246dae37bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: fides: image: ethyca/fides:local - command: uvicorn --host 0.0.0.0 --port 8080 --reload --reload-dir src fides.api.main:app + command: uvicorn --host 0.0.0.0 --port 8080 --reload --reload-dir src/fides/api fides.api.main:app healthcheck: test: [ "CMD", "curl", "-f", "http://0.0.0.0:8080/health" ] interval: 20s diff --git a/src/fides/core/user.py b/src/fides/core/user.py index b69361f04e..40d4aa4885 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -143,7 +143,8 @@ def create_command( username: str, password: str, first_name: str, last_name: str, server_url: str ) -> None: """ - Logic for creating a user from the CLI. + Given new user information, create a new user via the API using + the local credentials file. """ try: @@ -171,7 +172,8 @@ def create_command( def login_command(username: str, password: str, server_url: str) -> None: """ - Logic for logging in from the CLI. + Given a username and password, request an access_token from the API and + store all user information in a local credentials file. """ user_id, access_token = get_access_token( username=username, password=password, server_url=server_url @@ -186,7 +188,7 @@ def login_command(username: str, password: str, server_url: str) -> None: def get_permissions_command(server_url: str) -> None: """ - Logic for getting user permissions from the CLI. + Get user permissions from the API. """ credentials = read_credentials_file() user_id = credentials.user_id From 18bf535ccb000117bb2cc5dcd69733a5c7a74f19 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 15:43:27 +0800 Subject: [PATCH 30/33] fix: erroneous compose file change --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 246dae37bd..1f519f9878 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: fides: image: ethyca/fides:local - command: uvicorn --host 0.0.0.0 --port 8080 --reload --reload-dir src/fides/api fides.api.main:app + command: uvicorn --host 0.0.0.0 --port 8080 --reload --reload-dir src fides.api.main:app healthcheck: test: [ "CMD", "curl", "-f", "http://0.0.0.0:8080/health" ] interval: 20s From 639f202fd24ccdd5fc9e23cdf615ab374a246734 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 15:59:59 +0800 Subject: [PATCH 31/33] fix: static checks --- src/fides/cli/commands/user.py | 26 ++++++++++++-------------- src/fides/cli/options.py | 8 ++++---- src/fides/core/user.py | 4 ++-- tests/ctl/cli/test_cli.py | 2 +- tests/ctl/core/test_user.py | 3 ++- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 1ae45449ca..5d0c36704f 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -1,15 +1,13 @@ """Contains the user group of commands for fides.""" - -from typing import List - import click -from fides.core.user import ( - create_command, - login_command, - get_permissions_command, +from fides.cli.options import ( + first_name_option, + last_name_option, + password_option, + username_option, ) -from fides.cli.options import username, password, first_name, last_name +from fides.core.user import create_command, get_permissions_command, login_command @click.group(name="user") @@ -22,10 +20,10 @@ def user(ctx: click.Context) -> None: @user.command() @click.pass_context -@username -@password -@first_name -@last_name +@username_option +@password_option +@first_name_option +@last_name_option def create( ctx: click.Context, username: str, password: str, first_name: str, last_name: str ) -> None: @@ -47,8 +45,8 @@ def create( @user.command() @click.pass_context -@username -@password +@username_option +@password_option def login(ctx: click.Context, username: str, password: str) -> None: """ Use credentials to get a user access token and write it to a credentials file. diff --git a/src/fides/cli/options.py b/src/fides/cli/options.py index 8e5ac01b63..5700f3ec83 100644 --- a/src/fides/cli/options.py +++ b/src/fides/cli/options.py @@ -199,7 +199,7 @@ def prompt_password(ctx: click.Context, param: str, value: str) -> str: return value -def username(command: Callable) -> Callable: +def username_option(command: Callable) -> Callable: command = click.option( "-u", "--username", @@ -209,7 +209,7 @@ def username(command: Callable) -> Callable: return command -def password(command: Callable) -> Callable: +def password_option(command: Callable) -> Callable: command = click.option( "-p", "--password", @@ -219,7 +219,7 @@ def password(command: Callable) -> Callable: return command -def first_name(command: Callable) -> Callable: +def first_name_option(command: Callable) -> Callable: command = click.option( "-f", "--first-name", @@ -228,7 +228,7 @@ def first_name(command: Callable) -> Callable: return command -def last_name(command: Callable) -> Callable: +def last_name_option(command: Callable) -> Callable: command = click.option( "-l", "--last-name", diff --git a/src/fides/core/user.py b/src/fides/core/user.py index 40d4aa4885..df7b11ac0a 100644 --- a/src/fides/core/user.py +++ b/src/fides/core/user.py @@ -1,8 +1,8 @@ """Module for interaction with User endpoints/commands.""" import json +from os import getenv from pathlib import Path from typing import Dict, List, Tuple -from os import getenv import requests import toml @@ -10,8 +10,8 @@ from fides.cli.utils import handle_cli_response from fides.core.config import get_config -from fides.lib.cryptography.cryptographic_util import str_to_b64_str from fides.core.utils import echo_green, echo_red +from fides.lib.cryptography.cryptographic_util import str_to_b64_str from fides.lib.oauth.scopes import SCOPES config = get_config() diff --git a/tests/ctl/cli/test_cli.py b/tests/ctl/cli/test_cli.py index 8d6040bf25..c6673d5315 100644 --- a/tests/ctl/cli/test_cli.py +++ b/tests/ctl/cli/test_cli.py @@ -2,8 +2,8 @@ import os from base64 import b64decode from json import dump, loads -from typing import Generator from pathlib import Path +from typing import Generator import pytest from click.testing import CliRunner diff --git a/tests/ctl/core/test_user.py b/tests/ctl/core/test_user.py index 1cdca2025d..968cab82ee 100644 --- a/tests/ctl/core/test_user.py +++ b/tests/ctl/core/test_user.py @@ -1,6 +1,7 @@ -import pytest from os import environ +import pytest + from fides.core.user import Credentials, get_credentials_path From 8fd2c968f20a125e87a6243222c61ad4c036590d Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 16:14:30 +0800 Subject: [PATCH 32/33] docs: slight docstring tweak --- src/fides/cli/commands/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fides/cli/commands/user.py b/src/fides/cli/commands/user.py index 5d0c36704f..cae4f809e4 100644 --- a/src/fides/cli/commands/user.py +++ b/src/fides/cli/commands/user.py @@ -1,4 +1,4 @@ -"""Contains the user group of commands for fides.""" +"""Contains the user command group for the fides CLI.""" import click from fides.cli.options import ( From b286fae0cfbac8c614b0fc22cfcf1eb23151a050 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 16:21:27 +0800 Subject: [PATCH 33/33] docs: updated changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b65ac58089..d36f5fbfe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The types of changes are: ### Added +* Added the `user` command group to the CLI. [#2153](https://github.com/ethyca/fides/pull/2153) * Added the connection key to the execution log [#2100](https://github.com/ethyca/fides/pull/2100) * Added endpoints to retrieve DSR `Rule`s and `Rule Target`s [#2116](https://github.com/ethyca/fides/pull/2116) * Dataset classification UI now polls for results [#2123](https://github.com/ethyca/fides/pull/2123) @@ -138,7 +139,6 @@ The types of changes are: * Remove duplicate fastapi-caching and pin version. [#1765](https://github.com/ethyca/fides/pull/1765) - ## [2.2.0](https://github.com/ethyca/fides/compare/2.1.0...2.2.0) ### Added @@ -221,7 +221,6 @@ The types of changes are: * Bumped versions of packages that use OpenSSL [#1683](https://github.com/ethyca/fides/pull/1683) - ## [2.0.0](https://github.com/ethyca/fides/compare/1.9.6...2.0.0) ### Added