Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 22 additions & 142 deletions oks_cli/user.py
Original file line number Diff line number Diff line change
@@ -1,169 +1,49 @@
import click
from datetime import datetime
import dateutil.parser
import human_readable
import prettytable
import json
from prettytable import TableStyle

from nacl.public import PrivateKey, SealedBox
from nacl.encoding import Base64Encoder
from .utils import do_request, print_output, ctx_update, login_profile, profile_completer, \
find_project_id_by_name, project_completer

from .utils import do_request, print_output, find_project_id_by_name, ctx_update, login_profile, profile_completer, project_completer, JSONClickException

# DEIFNE THE USER COMMAND GROUP
@click.group(help="EIM users related commands.")
@click.group(help="User related commands.")
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
@click.pass_context
def user(ctx, project_name, profile):
"""Group of commands related to project management."""
"""Group of commands related to user management."""
ctx_update(ctx, project_name, None, profile)

# LIST USERS
@user.command('list', help="List EIM users")
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")

@user.command('types', help="List available user types")
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
@click.option('--profile', help="Configuration profile to use")
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format")
@click.option('--plain', is_flag=True, help="Plain table format")
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@click.pass_context
def user_list(ctx, output, project_name, profile):
"""List users"""
def user_types(ctx, project_name, output, plain, profile):
"""Display available user types."""
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
login_profile(profile)

project_id = find_project_id_by_name(project_name)

data = do_request("GET", f'projects/{project_id}/eim_users')
data = do_request("GET", f'projects/{project_id}/eim_users/types')

if output:
print_output(data, output)
return

field_names = ["USER", "ACCESS KEY", "STATE", "CREATED", "EXPIRATION DATE"]
table = prettytable.PrettyTable()
table.field_names = field_names

for user in data:
access_keys = user.get("AccessKeys", [])
access_key = access_keys[0] if access_keys else {}


state = access_key.get("State", "N/A")
if state == 'ACTIVE':
state = click.style(state, fg='green')
elif state == "INACTIVE":
state = click.style(state, fg='red')


row = [
user.get("UserName"),
access_key.get("AccessKeyId", "N/A"),
state
]

if "CreationDate" in access_key:
created_at = dateutil.parser.parse(access_key.get("CreationDate"))
now = datetime.now(tz=created_at.tzinfo)
row.append(human_readable.date_time(now - created_at))
else:
row.append("N/A")
table.field_names = ["USER TYPE", "DESCRIPTION"]

if "ExpirationDate" in access_key:
exp_at = dateutil.parser.parse(access_key.get("ExpirationDate"))
now = datetime.now(tz=exp_at.tzinfo)
row.append(human_readable.date_time(now - exp_at))
else:
row.append("N/A")
if plain:
table.set_style(TableStyle.PLAIN_COLUMNS)

table.add_row(row)
for entry in data:
table.add_row([
entry.get("UserType", ""),
entry.get("Description") or "-"
])

click.echo(table)


@user.command('create', help="Create a new EIM user")
@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer)
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
@click.option('--profile', help="Configuration profile to use")
@click.option('--user', '-u', required=True, help="OKS User type")
@click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w), by default is 1w")
@click.option('--nacl', is_flag=True, help="Use public key encryption on wire")
@click.pass_context
def user_create(ctx, project_name, output, profile, user, ttl, nacl):
"""Create a new EIM user."""
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
login_profile(profile)

project_id = find_project_id_by_name(project_name)

params = {
"user": user
}
if ttl:
params["ttl"] = ttl

if nacl:
ephemeral = PrivateKey.generate()
unsealbox = SealedBox(ephemeral)

headers = {
'x-encrypt-nacl': ephemeral.public_key.encode(Base64Encoder).decode('ascii')
}

raw_data = do_request(
"POST",
f'projects/{project_id}/eim_users',
params=params,
headers=headers
)

decrypted = unsealbox.decrypt(
raw_data.get("Data").encode('ascii'),
encoder=Base64Encoder
).decode('ascii')

data = json.loads(decrypted)

# format decrypted errors the same way as the api errors.
if "Errors" in data:
response_context = raw_data.get("ResponseContext")
errors = []
for error in data.get("Errors", []):
error["Code"] = str(data.get("Code"))
errors.append(error)

raise JSONClickException(json.dumps({"Errors": errors,"ResponseContext": response_context}, separators=(",", ":")))

else:
data = do_request(
"POST",
f'projects/{project_id}/eim_users',
params=params
)

print_output(data, output)

# DELETE USER
@user.command('delete', help="Delete an EIM user")
@click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
@click.option('--user', '-u', required=True, help="User name")
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format")
@click.option('--dry-run', is_flag=True, help="Run without any action")
@click.option('--force', is_flag=True, help="Force deletion without confirmation")
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@click.pass_context
def user_delete(ctx, project_name, user, output, dry_run, force, profile):
"""CLI command to delete an EIM user."""

project_name, _, profile = ctx_update(ctx, project_name, None, profile)
login_profile(profile)

project_id = find_project_id_by_name(project_name)

if dry_run:
message = {"message": f"Dry run: The user '{user}' would be deleted."}
print_output(message, output)
return

if force or click.confirm(f"Are you sure you want to delete the user '{user}'?", abort=True):
data = do_request("DELETE", f"projects/{project_id}/eim_users/{user}")
print_output(data, output)

8 changes: 2 additions & 6 deletions oks_cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,8 @@ def find_response_object(data):
return response["IP"]
elif key == "Nets":
return response["Nets"]
elif key == "EimUsers":
return response["EimUsers"]
elif key == "EimUser":
return response["EimUser"]
elif key == "Data":
return response
elif key == "EimUserTypes":
return response["EimUserTypes"]

raise click.ClickException("The API response format is incorrect.")

Expand Down
Loading