"""/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/ {"owner"} gives the bucket to another user's account DELETE /s3/buckets/ 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/ replace the account's secret; answers the new secret_key DELETE /s3/accounts/ 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))