119 lines
5.7 KiB
Python
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))
|