Skip to content

Commit

Permalink
Add DeleteAttribute support to the server
Browse files Browse the repository at this point in the history
This change adds DeleteAttribute operation support to the PyKMIP
server, supporting functionality unique to KMIP 1.0 - 1.4 and the
newer KMIP 2.0. Due to the current list of attributes supported
by the server, only multivalued attributes can currently be
deleted from a stored KMIP object. Over a dozen unit tests have
been added to verify the functionality of the new additions.

Partially implements #547
  • Loading branch information
PeterHamilton committed Nov 8, 2019
1 parent e48aff7 commit 676aaf5
Show file tree
Hide file tree
Showing 5 changed files with 920 additions and 4 deletions.
5 changes: 4 additions & 1 deletion kmip/pie/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ class ManagedObject(sql.Base):
_names = sqlalchemy.orm.relationship(
"ManagedObjectName",
back_populates="mo",
cascade="all, delete-orphan"
cascade="all, delete-orphan",
order_by="ManagedObjectName.id"
)
names = association_proxy('_names', 'name')
operation_policy_name = Column(
Expand All @@ -112,12 +113,14 @@ class ManagedObject(sql.Base):
"ApplicationSpecificInformation",
secondary=app_specific_info_map,
back_populates="managed_objects",
order_by="ApplicationSpecificInformation.id",
passive_deletes=True
)
object_groups = sqlalchemy.orm.relationship(
"ObjectGroup",
secondary=object_group_map,
back_populates="managed_objects",
order_by="ObjectGroup.id",
passive_deletes=True
)

Expand Down
167 changes: 167 additions & 0 deletions kmip/services/server/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,86 @@ def _set_attribute_on_managed_object(self, managed_object, attribute):
"The {0} attribute is unsupported.".format(attribute_name)
)

def _delete_attribute_from_managed_object(self, managed_object, attribute):
attribute_name, attribute_index, attribute_value = attribute
object_type = managed_object._object_type
if not self._attribute_policy.is_attribute_applicable_to_object_type(
attribute_name,
object_type
):
raise exceptions.ItemNotFound(
"The '{}' attribute is not applicable to '{}' objects.".format(
attribute_name,
''.join(
[x.capitalize() for x in object_type.name.split('_')]
)
)
)
if not self._attribute_policy.is_attribute_deletable_by_client(
attribute_name
):
raise exceptions.PermissionDenied(
"Cannot delete a required attribute."
)

if self._attribute_policy.is_attribute_multivalued(attribute_name):
# Get the specific attribute collection and attribute objects.
attribute_list = []
if attribute_name == "Name":
attribute_list = managed_object.names
if attribute_value is not None:
attribute_value = attribute_value.value
elif attribute_name == "Application Specific Information":
attribute_list = managed_object.app_specific_info
if attribute_value is not None:
namespace = attribute_value.application_namespace
attribute_value = objects.ApplicationSpecificInformation(
application_namespace=namespace,
application_data=attribute_value.application_data
)
elif attribute_name == "Object Group":
attribute_list = managed_object.object_groups
if attribute_value is not None:
attribute_value = objects.ObjectGroup(
object_group=attribute_value.value
)
else:
raise exceptions.InvalidField(
"The '{}' attribute is not supported.".format(
attribute_name
)
)

# Generically handle attribute deletion.
if attribute_value:
if attribute_list.count(attribute_value):
attribute_list.remove(attribute_value)
else:
raise exceptions.ItemNotFound(
"Could not locate the attribute instance with the "
"specified value: {}".format(attribute_value)
)
elif attribute_index is not None:
if attribute_index < len(attribute_list):
attribute_list.pop(attribute_index)
else:
raise exceptions.ItemNotFound(
"Could not locate the attribute instance with the "
"specified index: {}".format(attribute_index)
)
else:
# If no attribute index is provided, this is not a KMIP
# 1.* request. If no attribute value is provided, this
# must be a KMIP 2.0 attribute reference request, so
# delete all instances of the attribute.
attribute_list[:] = []
else:
# The server does not currently support any single-instance,
# client deletable attributes.
raise exceptions.InvalidField(
"The '{}' attribute is not supported.".format(attribute_name)
)

def _is_allowed_by_operation_policy(
self,
policy_name,
Expand Down Expand Up @@ -1075,6 +1155,8 @@ def _process_operation(self, operation, payload):
return self._process_create(payload)
elif operation == enums.Operation.CREATE_KEY_PAIR:
return self._process_create_key_pair(payload)
elif operation == enums.Operation.DELETE_ATTRIBUTE:
return self._process_delete_attribute(payload)
elif operation == enums.Operation.REGISTER:
return self._process_register(payload)
elif operation == enums.Operation.DERIVE_KEY:
Expand Down Expand Up @@ -1378,6 +1460,91 @@ def _process_create_key_pair(self, payload):
self._id_placeholder = str(private_key.unique_identifier)
return response_payload

@_kmip_version_supported('1.0')
def _process_delete_attribute(self, payload):
self._logger.info("Processing operation: DeleteAttribute")

unique_identifier = self._id_placeholder
if payload.unique_identifier:
unique_identifier = payload.unique_identifier

managed_object = self._get_object_with_access_controls(
unique_identifier,
enums.Operation.DELETE_ATTRIBUTE
)
deleted_attribute = None

attribute_name = None
attribute_index = None
attribute_value = None

if self._protocol_version >= contents.ProtocolVersion(2, 0):
# If the current attribute is defined, use that. Otherwise, use
# the attribute reference.
if payload.current_attribute:
try:
attribute_name = enums.convert_attribute_tag_to_name(
payload.current_attribute.attribute.tag
)
attribute_value = payload.current_attribute.attribute
except ValueError as e:
self._logger.exception(e)
raise exceptions.ItemNotFound(
"No attribute with the specified name exists."
)
elif payload.attribute_reference:
attribute_name = payload.attribute_reference.attribute_name
else:
raise exceptions.InvalidMessage(
"The DeleteAttribute request must specify the current "
"attribute or an attribute reference."
)
else:
# Build a partial attribute from the attribute name and index.
if payload.attribute_name:
attribute_name = payload.attribute_name
else:
raise exceptions.InvalidMessage(
"The DeleteAttribute request must specify the attribute "
"name."
)
if payload.attribute_index:
attribute_index = payload.attribute_index
else:
attribute_index = 0

# Grab a copy of the attribute before deleting it.
existing_attributes = self._get_attributes_from_managed_object(
managed_object,
[payload.attribute_name]
)
if len(existing_attributes) > 0:
if not attribute_index:
deleted_attribute = existing_attributes[0]
else:
if attribute_index < len(existing_attributes):
deleted_attribute = existing_attributes[
attribute_index
]
else:
raise exceptions.ItemNotFound(
"Could not locate the attribute instance with the "
"specified index: {}".format(attribute_index)
)

self._delete_attribute_from_managed_object(
managed_object,
(attribute_name, attribute_index, attribute_value)
)
self._data_session.commit()

response_payload = payloads.DeleteAttributeResponsePayload(
unique_identifier=unique_identifier,
attribute=deleted_attribute
)

return response_payload

@_kmip_version_supported('1.0')
def _process_register(self, payload):
self._logger.info("Processing operation: Register")
Expand Down
21 changes: 18 additions & 3 deletions kmip/services/server/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def __init__(self, version):
"""
self._version = version

# TODO (peterhamilton) Alphabetize these
self._attribute_rule_sets = {
'Unique Identifier': AttributeRuleSet(
True,
Expand Down Expand Up @@ -872,9 +873,9 @@ def __init__(self, version):
'Object Group': AttributeRuleSet(
False,
('server', 'client'),
False,
False,
False,
True,
True,
True,
True,
(
enums.Operation.CREATE,
Expand Down Expand Up @@ -1116,6 +1117,20 @@ def is_attribute_deprecated(self, attribute):
else:
return False

def is_attribute_deletable_by_client(self, attribute):
"""
Check if the attribute can be deleted by the client.
Args:
attribute (string): The name of the attribute (e.g., "Name").
Returns:
bool: True if the attribute can be deleted by the client. False
otherwise.
"""
rule_set = self._attribute_rule_sets.get(attribute)
return rule_set.deletable_by_client

def is_attribute_applicable_to_object_type(self, attribute, object_type):
"""
Check if the attribute is supported by the given object type.
Expand Down
Loading

0 comments on commit 676aaf5

Please sign in to comment.