diff --git a/kernelci/api/__init__.py b/kernelci/api/__init__.py index 44489138b0..94b56b38a7 100644 --- a/kernelci/api/__init__.py +++ b/kernelci/api/__init__.py @@ -70,15 +70,8 @@ def change_password(self, username: str, current: str, new: str) -> dict: """Change a password for a given user""" @abc.abstractmethod - def create_token(self, username: str, password: str, - scopes: Optional[Sequence[str]] = None) -> str: - """Create a new API token for the current user - - `scopes` contains optional security scope names which needs to be part - of API.security_scopes. Please note that user permissions can limit - the available scopes, for example only admin users can create admin - tokens. - """ + def create_token(self, username: str, password: str) -> dict: + """Create a new API token for the current user""" # ------- # Pub/Sub @@ -146,22 +139,22 @@ def get_groups( """Get user groups that match the provided attributes""" # ------------- - # User profiles + # User accounts # ------------- @abc.abstractmethod - def get_user_profiles( + def get_users( self, attributes: dict, offset: Optional[int] = None, limit: Optional[int] = None ) -> Sequence[dict]: - """Get user profiles that match the provided attributes""" + """Get user accounts that match the provided attributes""" @abc.abstractmethod - def create_user(self, username: str, password: str, profile: dict) -> dict: + def create_user(self, user: dict) -> dict: """Create a new user""" @abc.abstractmethod - def update_user(self, username: str, profile: dict) -> dict: + def update_user(self, user: dict) -> dict: """Update a user""" # ------------------------------------------------------------------------- @@ -180,20 +173,37 @@ def _get(self, path, params=None): resp.raise_for_status() return resp - def _post(self, path, data=None, params=None): + def _post(self, path, data=None, params=None, json_data=True): + url = self._make_url(path) + if json_data: + jdata = json.dumps(data) + resp = requests.post( + url, jdata, headers=self._headers, + params=params, timeout=self._timeout + ) + else: + self._headers['Content-Type'] = 'application/x-www-form-urlencoded' + resp = requests.post( + url, data, headers=self._headers, + params=params, timeout=self._timeout + ) + resp.raise_for_status() + return resp + + def _put(self, path, data=None, params=None): url = self._make_url(path) jdata = json.dumps(data) - resp = requests.post( + resp = requests.put( url, jdata, headers=self._headers, params=params, timeout=self._timeout ) resp.raise_for_status() return resp - def _put(self, path, data=None, params=None): + def _patch(self, path, data=None, params=None): url = self._make_url(path) jdata = json.dumps(data) - resp = requests.put( + resp = requests.patch( url, jdata, headers=self._headers, params=params, timeout=self._timeout ) diff --git a/kernelci/api/latest.py b/kernelci/api/latest.py index 461d249f37..2cef25e518 100644 --- a/kernelci/api/latest.py +++ b/kernelci/api/latest.py @@ -9,7 +9,6 @@ from typing import Optional, Sequence from cloudevents.http import from_json -import requests from . import API @@ -63,21 +62,12 @@ def change_password(self, username: str, current: str, new: str) -> dict: }, ).json() - def create_token(self, username: str, password: str, - scopes: Optional[Sequence[str]] = None) -> str: + def create_token(self, username: str, password: str) -> dict: data = { 'username': username, 'password': password, } - # The form field name is scope (in singular), but it is actually a long - # string with "scopes" separated by spaces. - # https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/#scope - if scopes: - data['scope'] = ' '.join(scopes) - url = self._make_url('/token') - resp = requests.post(url, data, timeout=self._timeout) - resp.raise_for_status() - return resp.json() + return self._post('/user/login', data, json_data=False).json() def subscribe(self, channel: str) -> int: resp = self._post(f'subscribe/{channel}') @@ -159,29 +149,19 @@ def get_groups( return self._get_api_objs(params=params, path='groups', limit=limit, offset=offset) - def get_user_profiles( + def get_users( self, attributes: dict, offset: Optional[int] = None, limit: Optional[int] = None ) -> Sequence[dict]: params = attributes.copy() if attributes else {} - return self._get_api_objs(params=params, path='users/profile', + return self._get_api_objs(params=params, path='users', limit=limit, offset=offset) - def create_user(self, username: str, password: str, profile: dict) -> dict: - data = { - 'password': password, - } - params = { - 'email': profile['email'], - } - return self._post(f'user/{username}', data, params) + def create_user(self, user: dict) -> dict: + return self._post('user/register', user).json() - def update_user(self, username: str, profile: dict) -> dict: - params = { - 'email': profile['email'], - 'groups': profile['groups'] - } - return self._put(f'user/{username}', params=params) + def update_user(self, user: dict) -> dict: + return self._patch('user/me', user).json() def get_api(config, token): diff --git a/kernelci/cli/user.py b/kernelci/cli/user.py index c3ca0cad10..f08ff57b7d 100644 --- a/kernelci/cli/user.py +++ b/kernelci/cli/user.py @@ -45,7 +45,7 @@ def find(attributes, config, api, indent): 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)) + users = api.get_users(split_attributes(attributes)) data = json.dumps(users, indent=indent) echo = click.echo_via_pager if len(users) > 1 else click.echo echo(data) @@ -56,14 +56,13 @@ def find(attributes, config, api, indent): @Args.config @Args.api @Args.indent -@click.option('--scope', multiple=True, help="Security scope(s)") -def token(username, config, api, indent, scope): +def token(username, config, api, indent): """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) + user_token = api.create_token(username, password) click.echo(json.dumps(user_token, indent=indent)) @@ -72,38 +71,54 @@ def user_password(): """Manage user passwords""" -@user_password.command -@click.argument('username') +@kci_user.command(secrets=True) +@click.option('--username') +@click.option('--email') +@click.option('--group', multiple=True, help="User group(s)") @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") +@Args.indent +def update(username, email, config, # pylint: disable=too-many-arguments + api, secrets, group, indent): + """Update own user account""" + user = {} + if username: + user['username'] = username + if email: + user['email'] = email + if group: + user['groups'] = group + if not user: + raise click.ClickException("Sorry, nothing to update") configs = kernelci.config.load(config) api_config = configs['api'][api] - api = kernelci.api.get_api(api_config) - api.change_password(username, current, new) + api = kernelci.api.get_api(api_config, secrets.api.token) + data = api.update_user(user) + click.echo(json.dumps(data, indent=indent)) @kci_user.command(secrets=True) @click.argument('username') @click.argument('email') +@click.option('--group', multiple=True, help="User group(s)") @Args.config @Args.api -def add(username, email, config, api, secrets): +@Args.indent +def add(username, email, config, # pylint: disable=too-many-arguments + api, secrets, group, indent): """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") + user = { + 'username': username, + 'email': email, + 'password': password, + 'groups': group + } 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) + data = api.create_user(user) + click.echo(json.dumps(data, indent=indent))