mirror of
				https://git.isptech.ru/ISPsystem/isp-maintenance.git
				synced 2025-10-30 07:03:02 +01:00 
			
		
		
		
	Add: auth cmd prototype, dummy_platform for api
This commit is contained in:
		| @@ -1,59 +0,0 @@ | ||||
| # ? docker-compose.yml for development environment | ||||
|  | ||||
| # ! To start development you need to create a directory ./dummy_platform. | ||||
| # ? Place files from the test platform into it: | ||||
| # ? VM6: | ||||
| # ?     /opt/ispsystem/vm/config.json - configuration file | ||||
| # ?     /opt/ispsystem/vm/mysql - database directory | ||||
| # ? DCI6: | ||||
| # ?     /opt/ispsystem/dci/config.json - configuration file | ||||
| # ?     /opt/ispsystem/dci/mysql - database directory | ||||
|  | ||||
| # ? Create ./.env file and fill it with required vars: | ||||
| # ? PLATFORM_TYPE='vm' | ||||
| # ? Database container: | ||||
| # ? MYSQL_DATABASE="database name" | ||||
| # ? MYSQL_ROOT_PASSWORD="super secret password from config.json" | ||||
|  | ||||
| # ? Launch:  | ||||
| # ?     docker-compose up -d --force-recreate | ||||
| # ?     docker attach mgrctl | ||||
|  | ||||
| services: | ||||
|   mgrctl: | ||||
|     container_name: mgrctl | ||||
|     restart: unless-stopped | ||||
|     build: | ||||
|       context: . | ||||
|       args: | ||||
|         - APP_VERSION=${APP_VERSION} | ||||
|         - APP_DIR=${APP_DIR} | ||||
|         - SRC_DIR=${SRC_DIR} | ||||
|         - PKG_NAME=${PKG_NAME} | ||||
|         - PKG_VERSION=${PKG_VERSION} | ||||
|     networks: | ||||
|       vm_box_net: null | ||||
|     volumes: | ||||
|       - type: bind | ||||
|         source: ./dummy_platform/opt/ispsystem/${PLATFORM_TYPE}/config.json | ||||
|         target: /opt/ispsystem/${PLATFORM_TYPE}/config.json | ||||
|     env_file: | ||||
|       - ./.env | ||||
|     tty: true | ||||
|     stdin_open: true | ||||
|   mysql: | ||||
|     container_name: mysql | ||||
|     image: docker-registry.ispsystem.com/mysql:5 | ||||
|     volumes: | ||||
|       - ./dummy_platform/opt/ispsystem/${PLATFORM_TYPE}/mysql:/var/lib/mysql | ||||
|     env_file: | ||||
|       - ./.env | ||||
|     labels: | ||||
|       autoconf_mysql: "true" | ||||
|     networks: | ||||
|       vm_box_net: null | ||||
|     command: --group-concat-max-len=131072 --max-connections=1000 --optimizer-search-depth=0 | ||||
|  | ||||
| networks: | ||||
|   vm_box_net: | ||||
|     driver: bridge | ||||
| @@ -3,18 +3,52 @@ import json | ||||
| import urllib | ||||
| import requests | ||||
|  | ||||
| from mgrctl.settings.api import INPUT_HOSTNAME, INPUT_PORT, HEADERS | ||||
| from mgrctl.settings.api import INPUT_URL, HEADERS | ||||
| from mgrctl.settings.platform import ( | ||||
|     PLATFORM_TYPE, | ||||
|     PLATFORM_VERIFY_SSL, | ||||
|     PLATFORM_DUMMY, | ||||
|     PLATFORM_DUMMY_VM6_API_URL, | ||||
|     PLATFORM_DUMMY_VM6_EMAIL, | ||||
|     PLATFORM_DUMMY_VM6_PASSWORD, | ||||
|     PLATFORM_DUMMY_VM6_TOKEN, | ||||
|     PLATFORM_DUMMY_DCI6_API_URL, | ||||
|     PLATFORM_DUMMY_DCI6_EMAIL, | ||||
|     PLATFORM_DUMMY_DCI6_PASSWORD, | ||||
|     PLATFORM_DUMMY_DCI6_TOKEN | ||||
| ) | ||||
|  | ||||
|  | ||||
| class BaseAPI(object): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|     def __init__(self): | ||||
|         """Announces required parameters""" | ||||
|         internal_api_url = f'http://{INPUT_HOSTNAME}:{INPUT_PORT}' | ||||
|         self.API_URL = api_url if api_url else internal_api_url | ||||
|         if PLATFORM_TYPE == 'vm': | ||||
|             if PLATFORM_DUMMY: | ||||
|                 self.API_URL = PLATFORM_DUMMY_VM6_API_URL | ||||
|                 self.AUTH_TYPE = 'Public' | ||||
|                 self.HEADERS = {'x-xsrf-token': PLATFORM_DUMMY_VM6_TOKEN} | ||||
|                 self.EMAIL = PLATFORM_DUMMY_VM6_EMAIL | ||||
|                 self.PASSWORD = PLATFORM_DUMMY_VM6_PASSWORD | ||||
|             else: | ||||
|                 self.API_URL = INPUT_URL | ||||
|                 self.AUTH_TYPE = 'Internal' | ||||
|                 self.HEADERS = HEADERS | ||||
|  | ||||
|         if PLATFORM_TYPE == 'dci': | ||||
|             if PLATFORM_DUMMY: | ||||
|                 self.API_URL = PLATFORM_DUMMY_DCI6_API_URL | ||||
|                 self.AUTH_TYPE = 'Public' | ||||
|                 self.HEADERS = {'x-xsrf-token': PLATFORM_DUMMY_DCI6_TOKEN} | ||||
|                 self.EMAIL = PLATFORM_DUMMY_DCI6_EMAIL | ||||
|                 self.PASSWORD = PLATFORM_DUMMY_DCI6_PASSWORD | ||||
|             else: | ||||
|                 self.API_URL = INPUT_URL | ||||
|                 self.AUTH_TYPE = 'Internal' | ||||
|                 self.HEADERS = HEADERS | ||||
|  | ||||
|         self.API_VERSION = 'v3' | ||||
|         self.API_DEFINITION = 'api' | ||||
|         self.HEADERS = HEADERS | ||||
|         self.VERIFY_SSL = verify_ssl | ||||
|         self.VERIFY_SSL = PLATFORM_VERIFY_SSL | ||||
|  | ||||
|     def _gen_request_url(self, url): | ||||
|         return f'{self.API_URL}/{self.API_DEFINITION}/{self.API_VERSION}{url}' | ||||
| @@ -61,69 +95,26 @@ class BaseAPI(object): | ||||
|             print(response) | ||||
|             raise sys.exit() | ||||
|  | ||||
|     def get(self, url, headers={}, data={}): | ||||
|         return self.call_api(url, headers, data, method='GET') | ||||
|  | ||||
|     def post(self, url, headers={}, data={}): | ||||
|         return self.call_api(url, headers, data, method='POST') | ||||
|  | ||||
|  | ||||
| class BaseAuthAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         """ | ||||
|         Init class for /auth/v4 requests | ||||
|  | ||||
|         Args: | ||||
|             api_url (str, optional): url api host. Defaults to None. | ||||
|             If None will use from settings.api.INPUT_HOSTNAME:INPUT_PORT | ||||
|  | ||||
|             verify_ssl (bool, optional): Isn't recommended. Defaults to True. | ||||
|             Use only for testing if you don't have root sign SSL cert. | ||||
|         """ | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_VERSION = 'v4' | ||||
|         self.API_DEFINITION = 'auth' | ||||
|  | ||||
|     def get_auth_token(self, email: str, password: str) -> dict: | ||||
|         """ | ||||
|         Get auth token for authentication | ||||
|  | ||||
|         Arg: | ||||
|             email (str): user email | ||||
|             password (str): user password | ||||
|  | ||||
|         Returns: | ||||
|             response (dict): { | ||||
|                 "confirmed": true, | ||||
|                 "expires_at": "date time", | ||||
|                 "id": "int", | ||||
|                 "token": "str" | ||||
|             } | ||||
|         """ | ||||
|     def get_auth_token(self, email=None, password=None) -> dict: | ||||
|         email = self.EMAIL if not email else email | ||||
|         password = self.PASSWORD if not password else password | ||||
|         return self.call_api( | ||||
|             url='/public/token', | ||||
|             method='POST', | ||||
|             data={'email': email, 'password': password} | ||||
|         ) | ||||
|  | ||||
|     def get_auth_key(self, token: str, user, auth_type='Internal') -> dict: | ||||
|         """ | ||||
|         Key authentication | ||||
|  | ||||
|         Args: | ||||
|             token (str): auth token | ||||
|             user (str or int): user id or email | ||||
|             auth_type (str, optional): May be Public for public auth. | ||||
|             Defaults to 'Internal'. | ||||
|  | ||||
|         Returns: | ||||
|             response (dict): { | ||||
|                 "id": "int", | ||||
|                 "key": "str" | ||||
|             } | ||||
|         """ | ||||
|     def get_auth_key(self, token=None, user=None) -> dict: | ||||
|         headers = {} | ||||
|         if auth_type == 'Public': | ||||
|         user = self.EMAIL if not user else user | ||||
|         if token: | ||||
|             headers = self.make_auth_header(token) | ||||
|         return self.call_api( | ||||
|             url=f'/user/{user}/key', | ||||
| @@ -132,32 +123,22 @@ class BaseAuthAPI(BaseAPI): | ||||
|         ) | ||||
|  | ||||
|     def make_auth_header(self, token: str) -> dict: | ||||
|         """ | ||||
|         Generate dict for auth header | ||||
|  | ||||
|         Args: | ||||
|             token (str): auth token | ||||
|  | ||||
|         Returns: | ||||
|             dict: {'x-xsrf-token': token} use it for request headers | ||||
|         """ | ||||
|         return {'x-xsrf-token': token} | ||||
|  | ||||
|     def whoami(self, token: str) -> dict: | ||||
|     def whoami(self) -> dict: | ||||
|         return self.call_api( | ||||
|             url='/whoami', | ||||
|             method='GET', | ||||
|             headers=self.make_auth_header(token) | ||||
|             method='GET' | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class BaseIpAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'ip' | ||||
|  | ||||
|  | ||||
| class BaseDnsProxyAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'dnsproxy' | ||||
|   | ||||
| @@ -6,8 +6,8 @@ class AuthAPI(BaseAuthAPI): | ||||
|  | ||||
|  | ||||
| class BackupAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_VERSION = 'v4' | ||||
|         self.API_DEFINITION = 'backup' | ||||
|  | ||||
| @@ -17,20 +17,20 @@ class DnsProxyAPI(BaseDnsProxyAPI): | ||||
|  | ||||
|  | ||||
| class EserviceAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'eservice' | ||||
|  | ||||
|  | ||||
| class IsoAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'iso' | ||||
|  | ||||
|  | ||||
| class IpmiAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'ipmiproxy' | ||||
|  | ||||
|  | ||||
| @@ -39,13 +39,13 @@ class IpAPI(BaseIpAPI): | ||||
|  | ||||
|  | ||||
| class ReportAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_VERSION = 'v4' | ||||
|         self.API_DEFINITION = 'report' | ||||
|  | ||||
|  | ||||
| class UpdaterAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'updater' | ||||
|   | ||||
| @@ -14,6 +14,6 @@ class IpAPI(BaseIpAPI): | ||||
|  | ||||
|  | ||||
| class VmAPI(BaseAPI): | ||||
|     def __init__(self, api_url=None, verify_ssl=True): | ||||
|         super().__init__(api_url, verify_ssl) | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.API_DEFINITION = 'vm' | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import click | ||||
|  | ||||
| from mgrctl.db.vm6.databases import isp_database | ||||
| from mgrctl.db.vm6.models import AuthUser | ||||
|  | ||||
| from mgrctl.apps.vm6.auth import __version__ | ||||
| from mgrctl.api.vm6 import AuthAPI | ||||
| from mgrctl.utils.api_users import UserAPI | ||||
|  | ||||
|  | ||||
| user_cursor = UserAPI(callback_class=AuthAPI) | ||||
|  | ||||
|  | ||||
| @click.group(help='auth cmd for auth in VMmanager 6') | ||||
| @@ -16,16 +18,79 @@ def cli(): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| @cli.command(help='show all users and their roles on platform (DEMO EXAMPLE)') | ||||
| def users(): | ||||
|     # check and init connection to db: | ||||
|     isp_database.connect() | ||||
|     # get all fields from auth_user table | ||||
|     # SELECT * FROM auth_user; | ||||
|     all_users = AuthUser.select() | ||||
|     # Iterate fields and print to console users' email and role | ||||
|     for user in all_users: | ||||
|         result = f'{user.email} | {user.roles[0]}' | ||||
|         click.echo(result) | ||||
|     # Close connection | ||||
|     isp_database.close() | ||||
| @cli.group(help='Manage users') | ||||
| def user(): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| @user.command(help='List users') | ||||
| @click.option( | ||||
|     '--all', | ||||
|     is_flag=True, | ||||
|     required=False, | ||||
|     help='Show all users' | ||||
| ) | ||||
| @click.option( | ||||
|     '--admins', | ||||
|     is_flag=True, | ||||
|     required=False, | ||||
|     help='Show all active admins', | ||||
| ) | ||||
| def ls(all, admins): | ||||
|     if all: | ||||
|         user_cursor.echo_users(role='all') | ||||
|     elif admins: | ||||
|         user_cursor.echo_users(role='admin') | ||||
|     else: | ||||
|         user_cursor.echo_users(role='all') | ||||
|  | ||||
|  | ||||
| @user.command(help='Generate access key and return auth link(s)') | ||||
| @click.option( | ||||
|     '--id', | ||||
|     required=False, | ||||
|     type=int, | ||||
|     help='User id' | ||||
| ) | ||||
| @click.option( | ||||
|     '--count', | ||||
|     required=False, | ||||
|     type=int, | ||||
|     help='Number of access keys generated', | ||||
| ) | ||||
| @click.option( | ||||
|     '--random', | ||||
|     is_flag=True, | ||||
|     required=False, | ||||
|     help='Interactive mode, ignores other keys' | ||||
| ) | ||||
| @click.option( | ||||
|     '--interactive', | ||||
|     is_flag=True, | ||||
|     required=False, | ||||
|     help='Interactive mode, ignores other keys' | ||||
| ) | ||||
| def access(id, count, interactive, random): | ||||
|     if id and not count: | ||||
|         keys = user_cursor.get_access_keys(user=id, count=1) | ||||
|         links = user_cursor.gen_access_links(keys) | ||||
|         user_cursor.echo_access_links(links) | ||||
|     elif id and count: | ||||
|         keys = user_cursor.get_access_keys(user=id, count=count) | ||||
|         links = user_cursor.gen_access_links(keys) | ||||
|         user_cursor.echo_access_links(links) | ||||
|     elif interactive: | ||||
|         pass | ||||
|     elif random: | ||||
|         admin = user_cursor.get_first_random_admin() | ||||
|         keys = user_cursor.get_access_keys(user=admin.get('id', 3), count=1) | ||||
|         links = user_cursor.gen_access_links(keys) | ||||
|         user_cursor.echo_access_links(links) | ||||
|     else: | ||||
|         pass | ||||
|  | ||||
|  | ||||
| @user.command(help='Generate API token for mgrctl user') | ||||
| def token(): | ||||
|     token = user_cursor.gen_api_token() | ||||
|     user_cursor.echo_api_token(token) | ||||
|   | ||||
| @@ -1,4 +1,8 @@ | ||||
| from mgrctl.settings.platform import PLATFORM_TYPE | ||||
| from requests.packages import urllib3 | ||||
| from mgrctl.settings.platform import ( | ||||
|     PLATFORM_TYPE, | ||||
|     PLATFORM_VERIFY_SSL_WARNING | ||||
| ) | ||||
|  | ||||
| # Name of nginx container: | ||||
| INPUT_HOSTNAME = 'input' if PLATFORM_TYPE == 'vm' else 'dci_input_1' | ||||
| @@ -6,5 +10,16 @@ INPUT_HOSTNAME = 'input' if PLATFORM_TYPE == 'vm' else 'dci_input_1' | ||||
| # Port that nginx container is listening: | ||||
| INPUT_PORT = '1500' | ||||
|  | ||||
| # Internal API url: | ||||
| INPUT_URL = f'http://{INPUT_HOSTNAME}:{INPUT_PORT}' | ||||
|  | ||||
| # Headers for internal auth: | ||||
| HEADERS = {"Internal-Auth": "on", "Accept": "application/json"} | ||||
|  | ||||
| # Suppress warning from urllib3: | ||||
| if not PLATFORM_VERIFY_SSL_WARNING: | ||||
|     # ! This is not recommended, | ||||
|     # ! the user will not see a message about an untrusted SSL connection | ||||
|     urllib3.disable_warnings( | ||||
|         category=urllib3.exceptions.InsecureRequestWarning | ||||
|     ) | ||||
|   | ||||
| @@ -4,6 +4,9 @@ from mgrctl.utils.helpers import parse_json_file | ||||
|  | ||||
| PLATFORM_TYPE = env.str('PLATFORM_TYPE', 'vm') | ||||
|  | ||||
| PLATFORM_VERIFY_SSL = env.bool('PLATFORM_VERIFY_SSL', True) | ||||
| PLATFORM_VERIFY_SSL_WARNING = env.bool('PLATFORM_VERIFY_SSL_WARNING', True) | ||||
|  | ||||
| PLATFORM_CONFIG = env.str( | ||||
|     'PLATFORM_CONFIG', | ||||
|     f'/opt/ispsystem/{PLATFORM_TYPE}/config.json' | ||||
| @@ -11,7 +14,20 @@ PLATFORM_CONFIG = env.str( | ||||
|  | ||||
| PLATFORM_CONFIG = parse_json_file(PLATFORM_CONFIG) | ||||
|  | ||||
| PLATFORM_URL = env.url( | ||||
| PLATFORM_URL = env.str( | ||||
|     'PLATFORM_URL', | ||||
|     f"https://{PLATFORM_CONFIG.get('DomainName' ,'replace.me')}" | ||||
| ) | ||||
|  | ||||
| # Development mode: | ||||
| PLATFORM_DUMMY = env.bool('PLATFORM_DUMMY', False) | ||||
|  | ||||
| PLATFORM_DUMMY_VM6_API_URL = env.str('PLATFORM_DUMMY_VM6_API_URL', '') | ||||
| PLATFORM_DUMMY_VM6_EMAIL = env.str('PLATFORM_DUMMY_VM6_EMAIL', '') | ||||
| PLATFORM_DUMMY_VM6_PASSWORD = env.str('PLATFORM_DUMMY_VM6_PASSWORD', '') | ||||
| PLATFORM_DUMMY_VM6_TOKEN = env.str('PLATFORM_DUMMY_VM6_TOKEN', '') | ||||
|  | ||||
| PLATFORM_DUMMY_DCI6_API_URL = env.str('PLATFORM_DUMMY_DCI6_API_URL', '') | ||||
| PLATFORM_DUMMY_DCI6_EMAIL = env.str('PLATFORM_DUMMY_DCI6_EMAIL', '') | ||||
| PLATFORM_DUMMY_DCI6_PASSWORD = env.str('PLATFORM_DUMMY_DCI6_PASSWORD', '') | ||||
| PLATFORM_DUMMY_DCI6_TOKEN = env.str('PLATFORM_DUMMY_DCI6_TOKEN', '') | ||||
|   | ||||
							
								
								
									
										74
									
								
								mgrctl/utils/api_users.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								mgrctl/utils/api_users.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import click | ||||
| from tabulate import tabulate | ||||
|  | ||||
| from mgrctl.settings.platform import PLATFORM_URL | ||||
|  | ||||
|  | ||||
| class UserAPI(object): | ||||
|     def __init__(self, callback_class): | ||||
|         """Announces required parameters""" | ||||
|         self.callback_class = callback_class | ||||
|         self.callback = callback_class() | ||||
|  | ||||
|     def get_users(self, role: str) -> dict: | ||||
|         data = {} | ||||
|         if role == 'admin': | ||||
|             data = {"where": "((roles+CP+'%@admin%')+AND+(state+EQ+'active'))"} | ||||
|         return self.callback.call_api( | ||||
|             url='/user', | ||||
|             method='GET', | ||||
|             data=data | ||||
|         ) | ||||
|  | ||||
|     def _format_users(self, users: dict) -> list: | ||||
|         output = [] | ||||
|         for user in users.get('list', []): | ||||
|             output.append({ | ||||
|                 'id': user.get('id', ''), | ||||
|                 'email': user.get('email', ''), | ||||
|                 'roles': user.get('roles', []), | ||||
|                 'state': user.get('state', '') | ||||
|             }) | ||||
|         return output | ||||
|  | ||||
|     def get_first_random_admin(self): | ||||
|         users = self.get_users(role='admin') | ||||
|         admin = {} | ||||
|         for user in users.get('list', []): | ||||
|             if '@admin' in admin.get('roles', []): | ||||
|                 admin = user | ||||
|                 break | ||||
|         return admin | ||||
|  | ||||
|     def echo_users(self, role: str) -> None: | ||||
|         users = self.get_users(role) | ||||
|         output = self._format_users(users) | ||||
|         click.echo(tabulate(output, headers='keys')) | ||||
|  | ||||
|     def get_access_keys(self, user, count=1): | ||||
|         keys = [] | ||||
|         while count >= 1: | ||||
|             count -= 1 | ||||
|             key = self.callback.get_auth_key(user=user) | ||||
|             check = key.get('key', '') | ||||
|             if check: | ||||
|                 keys.append(key) | ||||
|         return keys | ||||
|  | ||||
|     def gen_access_links(self, keys: list) -> list: | ||||
|         links = [] | ||||
|         for key in keys: | ||||
|             _id = key.get('id', '') | ||||
|             link = f"{PLATFORM_URL}/auth/key/{key.get('key', '')}" | ||||
|             links.append({'id': _id, 'link': link}) | ||||
|         return links | ||||
|  | ||||
|     def echo_access_links(self, links: list) -> None: | ||||
|         click.echo(tabulate(links, headers='keys')) | ||||
|  | ||||
|     def gen_api_token(self, email=None, password=None): | ||||
|         token = self.callback.get_auth_token(email, password) | ||||
|         return token | ||||
|  | ||||
|     def echo_api_token(self, token: dict) -> None: | ||||
|         click.echo(tabulate([token], headers='keys')) | ||||
		Reference in New Issue
	
	Block a user