Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding pagination for service credentials #360

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions confidant/routes/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
services_response_schema,
RevisionsResponse,
revisions_response_schema,
ServiceCredentialsResponse,
service_credentials_response_schema,
)
from confidant.services import (
credentialmanager,
Expand Down Expand Up @@ -283,6 +285,174 @@ def get_service(id):
return service_expanded_response_schema.dumps(service_response)


@blueprint.route('/v1/services/<id>/credentials', methods=['GET'])
@authnz.require_auth
def get_service_credentials(id):
'''
Get the credentials for the service with the provided ID.

**Example request**:

.. sourcecode:: http

GET /v1/services/example-development/credentials

:param id: The service ID to get.
:type id: str
:query boolean metadata_only: If true, only fetch metadata for this
service, and do not respond with decrypted credential pairs in the
credential responses.
:query boolean blind: If true, fetch blind credentials instead.
:query int page: the page to fetch, leave unspecified for first page.
:query int limit: the number of items per page (required for pagination).

**Example response**:

.. sourcecode:: http

HTTP/1.1 200 OK
Content-Type: application/json

{
"credentials": [
{
"id": "abcd12345bf4f1cafe8e722d3860404",
"name": "Example Credential",
"credential_keys": ["test_key"],
"credential_pairs": {
"test_key": "test_value"
},
"metadata": {
"example_metadata_key": "example_value"
},
"revision": 1,
"enabled": true,
"documentation": "Example documentation",
"modified_date": "2019-12-16T23:16:11.413299+00:00",
"modified_by": "rlane@example.com",
"permissions": {}
},
...
],
"blind_credentials": [],
"next_page": 2
}

:resheader Content-Type: application/json
:statuscode 200: Success
:statuscode 403: Client does not have permissions to get the service ID
provided.
'''
permissions = {
'metadata': False,
'get': False,
'update': False,
}
metadata_only = misc.get_boolean(request.args.get('metadata_only'))
logged_in_user = authnz.get_logged_in_user()
action = 'metadata' if metadata_only else 'get'
if action == 'metadata':
permissions['metadata'] = acl_module_check(
resource_type='service',
action='metadata',
resource_id=id,
)
elif action == 'get':
permissions['get'] = acl_module_check(
resource_type='service',
action='get',
resource_id=id,
)
if not permissions[action]:
msg = "{} does not have access to get service {}".format(
authnz.get_logged_in_user(),
id
)
error_msg = {'error': msg, 'reference': id}
return jsonify(error_msg), 403

logger.info(
'get_service called on id={} by user={} metadata_only={}'.format(
id,
logged_in_user,
metadata_only,
)
)
try:
service = Service.get(id)
if not authnz.service_in_account(service.account):
logger.warning(
'Authz failed for service {0} (wrong account).'.format(id)
)
msg = 'Authenticated user is not authorized.'
return jsonify({'error': msg}), 401
except DoesNotExist:
return jsonify({}), 404

if (service.data_type != 'service' and
service.data_type != 'archive-service'):
return jsonify({}), 404

logger.debug('Authz succeeded for service {0}.'.format(id))

limit = request.args.get(
'limit',
default=None,
type=int,
)
page = request.args.get(
'page',
default=None,
type=int
)
blind = request.args.get(
'blind',
default=False,
type=bool,
)

all_ids = service.credentials
next_page = None
if blind:
all_ids = service.blind_credentials

if limit:
query_items, next_page = misc.get_page(all_ids, limit, page)
else:
query_items = all_ids

try:
if blind:
credentials = credentialmanager.get_blind_credentials(query_items)
else:
credentials = credentialmanager.get_credentials(query_items)
except KeyError:
logger.exception('KeyError occurred in getting credentials')
return jsonify({'error': 'Decryption error.'}), 500

if authnz.user_is_user_type('user'):
permissions['update'] = acl_module_check(
resource_type='service',
action='update',
resource_id=id,
kwargs={
'credential_ids': query_items,
},
)
kwargs = {
'next_page': next_page,
'metadata_only': metadata_only
}
if blind:
kwargs['blind_credentials'] = credentials
else:
kwargs['credentials'] = credentials

return service_credentials_response_schema.dumps(
ServiceCredentialsResponse.from_credentials(**kwargs)
)


@blueprint.route('/v1/archive/services/<id>', methods=['GET'])
@authnz.require_auth
def get_archive_service_revisions(id):
Expand Down
58 changes: 58 additions & 0 deletions confidant/schema/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,63 @@ class Meta:
permissions = fields.Dict(keys=fields.Str(), values=fields.Boolean())


@attr.s
class ServiceCredentialsResponse(object):
next_page = attr.ib()
credentials = attr.ib(default=list)
blind_credentials = attr.ib(default=list)

@classmethod
def from_credentials(
cls,
credentials=None,
blind_credentials=None,
next_page=None,
metadata_only=True,
):
ret = cls(next_page)

if metadata_only:
include_sensitive = False
else:
include_sensitive = True

if credentials:
ret.credentials = [
CredentialResponse.from_credential(
credential,
include_credential_keys=True,
include_credential_pairs=include_sensitive,
)
for credential in credentials
]
if blind_credentials:
ret.blind_credentials = [
BlindCredentialResponse.from_blind_credential(
blind_credential,
include_credential_keys=True,
include_credential_pairs=include_sensitive,
include_data_key=include_sensitive,
)
for blind_credential in blind_credentials
]

return ret


class ServiceCredentialsResponseSchema(AutobuildSchema):
class Meta:
jit = toastedmarshmallow.Jit

_class_to_load = ServiceResponse

credentials = fields.List(fields.Nested(CredentialResponseSchema))
blind_credentials = fields.List(
fields.Nested(BlindCredentialResponseSchema)
)
next_page = fields.Int()


@attr.s
class ServicesResponse(object):
services = attr.ib()
Expand Down Expand Up @@ -228,5 +285,6 @@ def sort_revisions(self, item):


service_expanded_response_schema = ServiceExpandedResponseSchema()
service_credentials_response_schema = ServiceCredentialsResponseSchema()
services_response_schema = ServicesResponseSchema()
revisions_response_schema = RevisionsResponseSchema()
35 changes: 35 additions & 0 deletions confidant/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from datetime import datetime


split_cache = {}


def dict_deep_update(a, b):
"""
Deep merge in place of two dicts. For all keys in `b`, override matching
Expand Down Expand Up @@ -54,3 +57,35 @@ def utcnow():
"""
now = datetime.utcnow()
return now.replace(tzinfo=pytz.utc)


def _split_items(items, limit):
if limit not in split_cache:
split_cache[limit] = {}
key = str(items)
if key not in split_cache[limit]:
items = sorted(items)
split_cache[limit][key] = []
for i in range(0, len(items), limit):
split_cache[limit][key].append(items[i:i+limit])
return split_cache[limit][key]


def get_page(items, limit, page):
# no page specified (first page)
if page is None:
page = 1
pages = _split_items(items, limit)
total = len(pages)

# if there is one, calculate next page
# (consistent with other methods)
next_page = None
if page < total:
next_page = page + 1

# validate page within range
if 1 <= page <= total:
return _split_items(items, limit)[page-1], next_page
else:
return [], None
Loading