Files
linux-cloud-router/debian-package/src/usr/lib/vpn-router/vpnrouter_web/s3.py
T

119 lines
5.7 KiB
Python

"""/s3 and /account/s3: the S3-compatible object storage of the router (vpnrouter_s3), its buckets and its accounts.
GET /s3 {"settings", "endpoint", "version", "installed", "running", "buckets", "accounts",
"problem"}: the s3 section, the URL clients call, the pinned and the installed versions
of versitygw, whether it runs, the buckets, each {name, owner, access_key, size,
objects}, while it runs, the accounts, each {owner, access_key, created}, and why the
buckets cannot be read, null when they can
PUT /s3 change any of the settings of the s3 section, applied by vpn-router-setup
POST /s3/buckets make a bucket: {"name", "owner"}, owner a user with an S3 account
PUT /s3/buckets/<name> {"owner"} gives the bucket to another user's account
DELETE /s3/buckets/<name> remove the bucket, which must be empty
POST /s3/accounts make the S3 account of a user: {"owner"}; answers its secret_key, shown only here
PUT /s3/accounts/<owner> replace the account's secret; answers the new secret_key
DELETE /s3/accounts/<owner> remove the account
GET /account/s3 {"enabled", "endpoint", "region", "account"}: the signed-in user's account, or null
POST /account/s3 make the signed-in user's account; answers its secret_key
PUT /account/s3 replace its secret; answers the new secret_key
DELETE /account/s3 remove it
A change of the settings is applied by restarting vpn-router-setup without waiting for it.
"""
import ldap
import vpnrouter
import vpnrouter_s3
import vpnrouter_users
from .common import USER_STATUS, section_change
#: The HTTP status for each status of a vpnrouter_s3.Failure; any other is 500.
S3_STATUS = {2: 400, 3: 404, 4: 409}
def endpoint(cfg):
"""The URL S3 clients call: Caddy's site of the object storage on wan.local_fqdn."""
return f'https://{cfg["wan"]["local_fqdn"].strip()}:{cfg["s3"]["port"]}'
def answered(work):
"""The HTTP status and the JSON document of work(), a function that answers a document; its failures as errors."""
try:
return 200, work()
except vpnrouter_s3.Failure as failure:
return S3_STATUS.get(failure.status, 500), {'error': str(failure)}
except vpnrouter_users.Failure as failure:
return USER_STATUS.get(failure.status, 500), {'error': str(failure)}
except (ldap.LDAPError, OSError) as exc:
return 500, {'error': f'the object storage failed: {exc}'}
except SystemExit as exc:
return 500, {'error': str(exc.code)}
def overview(cfg):
"""The document of GET /s3."""
running = vpnrouter_s3.running()
found, problem = [], None
if running:
try:
found = vpnrouter_s3.buckets()
except vpnrouter_s3.Failure as failure:
problem = str(failure)
elif cfg['s3']['enabled']:
problem = f'{vpnrouter_s3.UNIT} does not run'
return {'settings': cfg['s3'], 'endpoint': endpoint(cfg), 'version': vpnrouter_s3.VERSION,
'installed': vpnrouter_s3.installed(), 'running': running, 'buckets': found,
'accounts': vpnrouter_s3.list_accounts(), 'problem': problem}
def s3(method, path, body):
"""Answer a /s3 request: the HTTP status and the JSON document."""
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
return 400, {'error': 'the body must be a JSON object'}
body = body or {}
parts = path.strip('/').split('/')
route = (method, *parts[1:2], len(parts))
try:
cfg = vpnrouter.load_config()
except SystemExit as exc:
return 500, {'error': str(exc.code)}
if route == ('GET', 1):
return answered(lambda: overview(cfg))
if route == ('PUT', 1):
changed = section_change('s3', body)
return changed if isinstance(changed, tuple) else (200, {'settings': changed})
if route == ('POST', 'buckets', 2):
return answered(lambda: vpnrouter_s3.bucket_add(body.get('name'), body.get('owner')))
if route == ('PUT', 'buckets', 3):
return answered(lambda: vpnrouter_s3.bucket_owner(parts[2], body.get('owner')))
if route == ('DELETE', 'buckets', 3):
return answered(lambda: vpnrouter_s3.bucket_remove(parts[2]))
if route == ('POST', 'accounts', 2):
return answered(lambda: vpnrouter_s3.account_add(body.get('owner')))
if route == ('PUT', 'accounts', 3):
return answered(lambda: vpnrouter_s3.account_rotate(parts[2]))
if route == ('DELETE', 'accounts', 3):
return answered(lambda: vpnrouter_s3.account_remove(parts[2]))
return 404, {'error': f'no route {method} {path}'}
def account_s3(name, method):
"""Answer a request of /account/s3 for the signed-in user: the HTTP status and the JSON document."""
try:
cfg = vpnrouter.load_config()
except SystemExit as exc:
return 500, {'error': str(exc.code)}
if method == 'GET':
return answered(lambda: {'enabled': cfg['s3']['enabled'], 'endpoint': endpoint(cfg),
'region': cfg['s3']['region'],
'account': next(iter(vpnrouter_s3.list_accounts(name)), None)})
if not cfg['s3']['enabled']:
return 409, {'error': 'the object storage is off; vpn-config-edit set s3.enabled true switches it on'}
actions = {'POST': vpnrouter_s3.account_add, 'PUT': vpnrouter_s3.account_rotate,
'DELETE': vpnrouter_s3.account_remove}
if method not in actions:
return 405, {'error': f'{method} is not allowed on /account/s3'}
return answered(lambda: actions[method](name))