Commit ce5fd15c authored by Alexander Philipp Nowosad's avatar Alexander Philipp Nowosad
Browse files

Merge branch 'feature/sync'

parents 6176ddf2 648e2ef0
Pipeline #138099 failed with stages
in 17 minutes and 24 seconds
......@@ -14,6 +14,9 @@
"prettier"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"no-return-await": "off",
"@typescript-eslint/return-await": "error",
"@angular-eslint/component-selector": [
"error",
{
......
......@@ -52,3 +52,6 @@ Thumbs.db
# problems
test.js
# CI npm folder
.npm
# Source: https://github.com/github/gitignore/blob/master/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
import click
from http import HTTPStatus
from group_management.couchdb import Database, Server, User
from group_management import management, utils
@click.group(context_settings={ 'auto_envvar_prefix': 'MGMT' })
@click.option('-u', '--username', required=True)
@click.password_option(confirmation_prompt=False)
@click.argument('address')
@click.pass_context
def cli(ctx, username, password, address):
ctx.ensure_object(Server)
ctx.call_on_close(lambda: ctx.obj.close())
ctx.obj.address = address
ctx.obj.login(username, password)
@cli.command()
@click.argument('usersfile', type=click.File())
@click.argument('passwordsfile', type=click.File('w'))
@click.argument('initdata', type=click.File('rb'), nargs=-1)
@click.make_pass_decorator(Server)
def init(server, usersfile, passwordsfile, initdata):
management.init_server(server, usersfile, passwordsfile, initdata)
@cli.command()
@click.option('--set',
type=click.Choice(['true', 'nolb', 'false'],
case_sensitive=False))
@click.make_pass_decorator(Server)
def maintenance(server, set):
if set is not None:
management.set_server_maintenance(server, set)
click.secho('Set maintenance mode {}'.format(set), fg='green')
else:
result, mode = management.check_server_maintenance_mode(server)
if result:
click.secho('Server is up and running', fg='green')
else:
click.secho(
'Server is in maintenance mode {}'.format(mode), fg='red'
)
@cli.group()
@click.argument('name')
@click.make_pass_decorator(Server)
@click.pass_context
def db(ctx, server, name):
ctx.ensure_object(Database)
ctx.obj.name = name
ctx.obj.server = server
@db.command()
@click.argument('initdata', type=click.File('rb'), nargs=-1)
@click.make_pass_decorator(Database)
def init(database, initdata):
management.init_db(database, initdata)
click.secho(
'Sucessfully initialized database {}.'.format(database.name),
fg='green'
)
@cli.command()
@click.make_pass_decorator(Server)
def users(server):
user_groups = management.users_list(server)
stdout = click.get_text_stream('stdout')
writer = utils.csv_writer(stdout)
for user in user_groups:
writer.writerow([
user['username'],
user['group'] if user['group'] is not None else '-'
])
@cli.group()
@click.argument('name')
@click.make_pass_decorator(Server)
@click.pass_context
def user(ctx, server, name):
ctx.ensure_object(User)
ctx.obj.name = name
ctx.obj.server = server
@user.command()
@click.make_pass_decorator(User)
def create(user):
password = management.create_user(user)
click.secho(
'Sucessfully created user {} with password {}.'.format(
user.name, password
),
fg='green'
)
@user.command()
@click.argument('group', required=False)
@click.make_pass_decorator(User)
def group(user, group):
if group is None:
group = management.get_group(user)
click.echo('User {} is in group {}.'.format(user.name, group))
else:
management.set_group(user, group)
@user.command()
@click.make_pass_decorator(User)
def resetpassword(user):
password = management.reset_password(user)
click.secho(
'Sucessfully changed password of user {}.'.format(user.name),
fg='green'
)
click.secho(
'The new password is {}.'.format(password),
fg='green'
)
from http import HTTPStatus
import requests
from urllib.parse import urljoin, quote
class CouchException(Exception):
def __init__(self, response):
self.response = response
class Server:
def __init__(self):
self.address = None
self.session = requests.Session()
def login(self, username, password):
self.session.auth = (username, password)
def up(self):
url = urljoin(self.address, '_up/')
response = self.session.get(url)
if response.status_code == HTTPStatus.OK:
return (True, response.json()['status'])
if response.status_code == HTTPStatus.NOT_FOUND:
return (False, response.json()['status'])
raise CouchException(response)
def maintenance(self, value):
url = urljoin(
self.address,
'_node/_local/_config/couchdb/maintenance_mode/'
)
response = self.session.put(url, json=value)
if response.status_code == HTTPStatus.OK:
return
raise CouchException(response)
def close(self):
self.session.close()
class Database:
def __init__(self):
self.name = None
self.server = None
def _get_endpoint_url(self, endpoint):
return urljoin(
urljoin(self.server.address, self.name + '/'), endpoint
)
def exists(self):
url = urljoin(self.server.address, self.name)
response = self.server.session.head(url)
if response.status_code == HTTPStatus.OK:
return True
if response.status_code == HTTPStatus.NOT_FOUND:
return False
raise CouchException(response)
def all_docs(self, include_docs=False):
url = self._get_endpoint_url('_all_docs/')
response = self.server.session.get(url, params={
'include_docs': include_docs,
})
if response.status_code == HTTPStatus.OK:
return response.json()
raise CouchException(response)
def create(self):
url = urljoin(self.server.address, self.name)
response = self.server.session.put(url)
if (
response.status_code == HTTPStatus.CREATED or
response.status_code == HTTPStatus.ACCEPTED
):
return
raise CouchException(response)
def create_if_not_exists(self):
try:
self.create()
except CouchException as e:
if e.response.status_code == HTTPStatus.PRECONDITION_FAILED:
return
else:
raise e
def create_document(self, doc):
url = urljoin(self.server.address, self.name)
response = self.server.session.post(url, json=doc)
if (
response.status_code == HTTPStatus.CREATED or
response.status_code == HTTPStatus.ACCEPTED
):
return
raise CouchException(response)
def update_document(self, doc):
url = self._get_endpoint_url(quote(doc['_id']) + '/')
response = self.server.session.put(url, json=doc)
if (
response.status_code == HTTPStatus.CREATED or
response.status_code == HTTPStatus.ACCEPTED
):
return
raise CouchException(response)
def get_document(self, id):
url = self._get_endpoint_url(quote(id) + '/')
response = self.server.session.get(url)
if response.status_code == HTTPStatus.OK:
return response.json()
raise CouchException(response)
def bulk_create(self, docs):
url = self._get_endpoint_url('_bulk_docs/')
response = self.server.session.post(url, json={ 'docs': docs })
if response.status_code == HTTPStatus.CREATED:
return
raise CouchException(response)
def security(self, security):
url = self._get_endpoint_url('_security/')
response = self.server.session.put(url, json=security)
if response.status_code == HTTPStatus.OK:
return
raise CouchException(response)
class User:
def __init__(self):
self.name = None
self.database = Database()
self.database.name = '_users'
@property
def id(self):
return 'org.couchdb.user:{}'.format(self.name)
@property
def server(self):
return self.database.server
@server.setter
def server(self, server):
self.database.server = server
def info(self):
return self.database.get_document(self.id)
def create(self, password, roles=[]):
self.database.create_document({
'_id': self.id,
'name': self.name,
'password': password,
'roles': roles,
'type': 'user'
})
def create_if_not_exists(self, *args, **kwargs):
try:
self.create(*args, **kwargs)
except CouchException as e:
if e.response.status_code == HTTPStatus.CONFLICT:
return
else:
raise e
def roles(self, roles=[]):
current = self.database.get_document(self.id)
current['roles'] = roles
self.database.update_document(current)
def change_password(self, password):
current = self.database.get_document(self.id)
current['password'] = password
self.database.update_document(current)
from collections import defaultdict
from group_management import utils
from group_management.couchdb import CouchException, Database, User
from http import HTTPStatus
import re
class Expressions:
GROUP = re.compile('^group[0-9]+$')
class ManagementException(Exception):
def __init__(self, message):
self.message = message
def check_server_maintenance_mode(server):
return server.up()
def set_server_maintenance(server, value):
server.maintenance(value)
def init_db(database, initdata):
if not Expressions.GROUP.match(database.name):
raise ManagementException(
'Database for group should have the form "group[0-9]+"'
)
database.create_if_not_exists()
if len(initdata) > 0:
database.bulk_create(utils.import_docs(initdata))
database.security({
'members': { 'roles': ['_admin', database.name, 'mgmt_backup'] },
'admins': { 'roles': ['_admin'] }
})
def users_list(server):
database = Database()
database.server = server
database.name = '_users'
users = filter(
lambda doc: doc['id'].startswith('org.couchdb.user:'),
database.all_docs(include_docs=True)['rows']
)
user_groups = []
for user in users:
doc = user['doc']
try:
group = next(filter(Expressions.GROUP.match, doc['roles']))
except StopIteration:
group = None
user_groups.append({
'username': doc['name'],
'group': group,
})
return user_groups
def create_user(user):
password = utils.random_password()
user.create_if_not_exists(password)
return password
def set_group(user, group):
if not Expressions.GROUP.match(group):
raise ManagementException('Group has to have the form "group[0-9]+"')
user.roles([group, 'mgmt_pwd'])
def get_group(user):
info = user.info()
return next(filter(Expressions.GROUP.match, info['roles']))
def reset_password(user):
password = utils.random_password()
user.change_password(password)
return password
def init_server(server, usersfile, passwordsfile, initdata):
writer = utils.csv_writer(passwordsfile)
users = utils.import_csv(usersfile)
groups = defaultdict(list)
for i, (username, group) in enumerate(users):
if not Expressions.GROUP.match(group):
raise ManagementException(
'Group has to have the form "group[0-9]+" in line {}'.format(
i + 1
)
)
groups[group].append(username)
for group, users in groups.items():
database = Database()
database.server = server
database.name = group
if not database.exists():
init_db(database, initdata)
for username in users:
user = User()
user.server = server
user.name = username
password = utils.random_password()
try:
user.create(password=password, roles=[group, 'mgmt_pwd'])
except CouchException as e:
if e.response.status_code == HTTPStatus.CONFLICT:
user.roles([group, 'mgmt_pwd'])
writer.writerow([username, group, '-'])
else:
raise e
else:
writer.writerow([username, group, password])
import csv
import json
import string
import secrets
def import_docs(buffers):
docs = []
for buffer in buffers:
docs.extend(json.load(buffer))
return docs
def import_csv(buffer):
return csv.reader(buffer)
def csv_writer(buffer):
return csv.writer(buffer)
# https://docs.python.org/3/library/secrets.html#recipes-and-best-practices
def random_password():
letters = string.ascii_letters + string.digits
return ''.join(secrets.choice(letters) for i in range(12))
certifi==2021.5.30
charset-normalizer==2.0.4
click==8.0.1
idna==3.2
requests==2.26.0
urllib3==1.26.6
from setuptools import find_packages, setup