Skip to content
Merged
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
20 changes: 2 additions & 18 deletions kci
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,12 @@ features. See the online documentation for more details:
https://kernelci.org/docs/api/.
"""

import json

import click

from kernelci.cli import Args, kci
from kernelci.cli import kci
from kernelci.cli import ( # pylint: disable=unused-import
docker as kci_docker,
user as kci_user,
)
Comment on lines 18 to 21
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unused imports.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it is required to register sub-commands. A comment about it would have been helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's quite implicit, which is nice in a way as it means no additional code is required but it's against the Zen of Python. I'll add some comments there.

import kernelci.api
import kernelci.config


@kci.command(secrets=True)
@Args.config
@Args.api
def whoami(config, api, secrets):
"""Use whoami to get current user info with API authentication"""
configs = kernelci.config.load(config)
api_config = configs['api_configs'][api]
api = kernelci.api.get_api(api_config, secrets.api.token)
data = api.whoami()
click.echo(json.dumps(data, indent=2))


if __name__ == '__main__':
Expand Down
109 changes: 109 additions & 0 deletions kernelci/cli/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Copyright (C) 2023 Collabora Limited
# Author: Guillaume Tucker <[email protected]>
# Author: Jeny Sadadia <[email protected]>
# Author: Paweł Wieczorek <[email protected]>

"""Tool to manage KernelCI API users"""

import getpass
import json

import click

import kernelci.api
import kernelci.config
from . import Args, kci, split_attributes


@kci.group(name='user')
def kci_user():
"""Manage user accounts"""


@kci_user.command(secrets=True)
@Args.config
@Args.api
@Args.indent
def whoami(config, api, indent, secrets):
"""Get the current user's details with API authentication"""
configs = kernelci.config.load(config)
api_config = configs['api'][api]
api = kernelci.api.get_api(api_config, secrets.api.token)
data = api.whoami()
click.echo(json.dumps(data, indent=indent))


@kci_user.command
@click.argument('attributes', nargs=-1)
@Args.config
@Args.api
@Args.indent
def find(attributes, config, api, indent):
"""Find user profiles with arbitrary attributes"""
configs = kernelci.config.load(config)
api_config = configs['api'][api]
api = kernelci.api.get_api(api_config)
users = api.get_user_profiles(split_attributes(attributes))
data = json.dumps(users, indent=indent)
echo = click.echo_via_pager if len(users) > 1 else click.echo
echo(data)


@kci_user.command
@click.argument('username')
@Args.config
@Args.api
@Args.indent
@click.option('--scope', multiple=True, help="Security scope(s)")
def token(username, config, api, indent, scope):
"""Create a new API token using a user name and password"""
password = getpass.getpass()
configs = kernelci.config.load(config)
api_config = configs['api'][api]
api = kernelci.api.get_api(api_config)
user_token = api.create_token(username, password, scope)
click.echo(json.dumps(user_token, indent=indent))


@kci_user.group(name='password')
def user_password():
"""Manage user passwords"""
Comment on lines +70 to +72
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After I added sub-commands to this group, it became kci user password reset-password.
I expected it to be kci password reset-password.
What are the benefits of adding groups?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this was as per the proposed syntax on the GitHub discussion:
kernelci/kernelci-project#263 (comment)

It means all the user-related commands are grouped under kci user. There might be other passwords for other things, like kci storage password or anything else.



@user_password.command
@click.argument('username')
@Args.config
@Args.api
def update(username, config, api):
"""Update the password for a given user"""
current = getpass.getpass("Current password: ")
new = getpass.getpass("New password: ")
retyped = getpass.getpass("Retype new password: ")
if new != retyped:
raise click.ClickException("Sorry, passwords do not match")
configs = kernelci.config.load(config)
api_config = configs['api'][api]
api = kernelci.api.get_api(api_config)
api.change_password(username, current, new)


@kci_user.command(secrets=True)
@click.argument('username')
@click.argument('email')
@Args.config
@Args.api
def add(username, email, config, api, secrets):
"""Add a new user account"""
profile = {
'email': email,
}
password = getpass.getpass()
retyped = getpass.getpass("Confirm password: ")
if password != retyped:
raise click.ClickException("Sorry, passwords do not match")
configs = kernelci.config.load(config)
api_config = configs['api'][api]
api = kernelci.api.get_api(api_config, secrets.api.token)
api.create_user(username, password, profile)
91 changes: 0 additions & 91 deletions kernelci/legacy/cli/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,10 @@

"""Tool to manage KernelCI API users"""

import getpass

from .base import Args, sub_main
from .base_api import APICommand, AttributesCommand


class cmd_whoami(APICommand): # pylint: disable=invalid-name
"""Use the /whoami entry point to get the current user's data"""
args = APICommand.args + [Args.api_token]
opt_args = APICommand.opt_args + [Args.indent]

def _api_call(self, api, configs, args):
data = api.whoami()
self._print_json(data, args.indent)
return True


class cmd_get_token(APICommand): # pylint: disable=invalid-name
"""Create a new API token for the current user"""
args = APICommand.args + [Args.username]
opt_args = APICommand.opt_args + [
{
'name': '--scopes',
'action': 'append',
'help': "Security scopes",
},
]

def _api_call(self, api, configs, args):
password = getpass.getpass()
token = api.create_token(args.username, password, args.scopes)
self._print_json(token, args.indent)
return True


class cmd_password_hash(APICommand): # pylint: disable=invalid-name
"""Get an encryption hash for an arbitrary password"""

def _api_call(self, api, configs, args):
password = getpass.getpass()
print(api.password_hash(password))
return True


class cmd_change_password(APICommand): # pylint: disable=invalid-name
"""Change a password for a given user"""
args = APICommand.args + [Args.username]

def _api_call(self, api, configs, args):
current = getpass.getpass("Current password: ")
new = getpass.getpass("New password: ")
retyped = getpass.getpass("Retype new password: ")
if new != retyped:
print("Sorry, passwords do not match.")
return False
api.change_password(args.username, current, new)
return True


class cmd_get_group(APICommand): # pylint: disable=invalid-name
"""Get a user group with a given ID"""
args = APICommand.args + [Args.group_id]
Expand All @@ -89,42 +34,6 @@ def _api_call(self, api, configs, args):
return True


class cmd_add(APICommand): # pylint: disable=invalid-name
"""Add a new user account"""
args = APICommand.args + [
Args.api_token,
{
'name': 'username',
'help': "Username of the new user",
},
{
'name': 'email',
'help': "Email address of the new user",
},
]

def _api_call(self, api, configs, args):
profile = {
'email': args.email,
}
password = getpass.getpass()
api.create_user(args.username, password, profile)
return True


class cmd_find_users(AttributesCommand): # pylint: disable=invalid-name
"""Find user profiles with arbitrary attributes"""
opt_args = AttributesCommand.opt_args + [
Args.limit, Args.offset
]

def _api_call(self, api, configs, args):
attributes = self._split_attributes(args.attributes)
profiles = api.get_user_profiles(attributes, args.offset, args.limit)
self._print_json(profiles, args.indent)
return True


class cmd_update(APICommand): # pylint: disable=invalid-name
"""Update a new user account"""
args = APICommand.args + [
Expand Down