diff --git a/kmip/core/attributes.py b/kmip/core/attributes.py index d2788688..8f7511c7 100644 --- a/kmip/core/attributes.py +++ b/kmip/core/attributes.py @@ -1568,3 +1568,213 @@ def __str__(self): 'salt': self.salt, 'iteration_count': self.iteration_count }) + + +class AlternativeName(primitives.Struct): + """ + The AlternativeName attribute is used to identify and locate the object. + + This attribute is assigned by the client, and the Alternative Name Value + is intended to be in a form that humans are able to interpret. The key + management system MAY specify rules by which the client creates valid + alternative names. Clients are informed of such rules by a mechanism that + is not specified by this standard. Alternative Names MAY NOT be unique + within a given key management server. + + Attributes: + alternative_name_value: text string + alternative_name_type: enumeration + + See Section 4.2 of the KMIP v2.1 specification for more information. + """ + + def __init__( + self, + alternative_name_value=None, + alternative_name_type=None + ): + """ + Construct an AlternativeName object. + + Args: + alternative_name_value (string): Optional, defaults to None. + Required for read/write. + alternative_name_type (enumeration): Optional, defaults to None. + Required for read/write. + """ + super(AlternativeName, self).__init__( + enums.Tags.ALTERNATIVE_NAME + ) + + self._alternative_name_value = None + self._alternative_name_type = None + + self.alternative_name_value = alternative_name_value + self.alternative_name_type = alternative_name_type + + @property + def alternative_name_value(self): + if self._alternative_name_value: + return self._alternative_name_value.value + return None + + @alternative_name_value.setter + def alternative_name_value(self, value): + if value is None: + self._alternative_name_value = None + elif isinstance(value, six.string_types): + self._alternative_name_value = primitives.TextString( + value=value, + tag=enums.Tags.ALTERNATIVE_NAME_VALUE + ) + else: + raise TypeError("The alternative name value must be a string.") + + @property + def alternative_name_type(self): + if self._alternative_name_type: + return self._alternative_name_type.value + return None + + @alternative_name_type.setter + def alternative_name_type(self, value): + if value is None: + self._alternative_name_type = None + elif isinstance(value, enums.AlternativeNameType): + self._alternative_name_type = primitives.Enumeration( + enum=enums.AlternativeNameType, + tag=enums.Tags.ALTERNATIVE_NAME_TYPE, + value=value + ) + else: + raise TypeError("The alternative name type must be an int.") + + def read(self, input_buffer, kmip_version=enums.KMIPVersion.KMIP_1_0): + """ + Read the data encoding the AlternativeName attribute + and decode it. + + Args: + input_buffer (stream): A data stream containing encoded object + data, supporting a read method; usually a BytearrayStream + object. + kmip_version (KMIPVersion): An enumeration defining the KMIP + version with which the object will be decoded. Optional, + defaults to KMIP 1.0. + """ + super(AlternativeName, self).read( + input_buffer, + kmip_version=kmip_version + ) + local_buffer = utils.BytearrayStream(input_buffer.read(self.length)) + + if self.is_tag_next(enums.Tags.ALTERNATIVE_NAME_VALUE, local_buffer): + self._alternative_name_value = primitives.TextString( + tag=enums.Tags.ALTERNATIVE_NAME_VALUE + ) + self._alternative_name_value.read( + local_buffer, + kmip_version=kmip_version + ) + else: + raise exceptions.InvalidKmipEncoding( + "The AlternativeName encoding is missing the " + "AlternativeNameValue field." + ) + if self.is_tag_next(enums.Tags.ALTERNATIVE_NAME_TYPE, local_buffer): + self._alternative_name_type = primitives.Enumeration( + enum=enums.AlternativeNameType, + tag=enums.Tags.ALTERNATIVE_NAME_TYPE + ) + self._alternative_name_type.read( + local_buffer, + kmip_version=kmip_version + ) + else: + raise exceptions.InvalidKmipEncoding( + "The AlternativeName encoding is missing the " + "AlternativeNameType field." + ) + + self.is_oversized(local_buffer) + + def write(self, output_buffer, kmip_version=enums.KMIPVersion.KMIP_1_0): + """ + Write the data encoding the AlternativeName object to a + buffer. + + Args: + output_buffer (stream): A data stream in which to encode object + data, supporting a write method; usually a BytearrayStream + object. + kmip_version (KMIPVersion): An enumeration defining the KMIP + version with which the object will be encoded. Optional, + defaults to KMIP 1.0. + """ + local_buffer = utils.BytearrayStream() + + if self._alternative_name_value: + self._alternative_name_value.write( + local_buffer, + kmip_version=kmip_version + ) + else: + raise exceptions.InvalidField( + "The AlternativeName object is missing the " + "AlternativeNameValue field." + ) + if self._alternative_name_type: + self._alternative_name_type.write( + local_buffer, + kmip_version=kmip_version + ) + else: + raise exceptions.InvalidField( + "The AlternativeName object is missing the " + "AlternativeNameType field." + ) + + self.length = local_buffer.length() + super(AlternativeName, self).write( + output_buffer, + kmip_version=kmip_version + ) + output_buffer.write(local_buffer.buffer) + + def __repr__(self): + args = [ + "alternative_name_value={}".format( + repr(self.alternative_name_value) + ), + "alternative_name_type={}".format(repr(self.alternative_name_type)) + ] + return "AlternativeName({})".format(", ".join(args)) + + def __str__(self): + value = ", ".join( + [ + '"alternative_name_value": "{}"'.format( + self.alternative_name_value + ), + '"alternative_name_type": "{}"'.format( + self.alternative_name_type + ) + ] + ) + return "{" + value + "}" + + def __eq__(self, other): + if isinstance(other, AlternativeName): + if self.alternative_name_value != other.alternative_name_value: + return False + if self.alternative_name_type != other.alternative_name_type: + return False + return True + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, AlternativeName): + return not self.__eq__(other) + else: + return NotImplemented diff --git a/kmip/core/factories/attribute_values.py b/kmip/core/factories/attribute_values.py index 4a1c4a59..6c86a910 100644 --- a/kmip/core/factories/attribute_values.py +++ b/kmip/core/factories/attribute_values.py @@ -108,6 +108,8 @@ def create_attribute_value(self, name, value): return primitives.Boolean(value, enums.Tags.SENSITIVE) elif name is enums.AttributeType.CUSTOM_ATTRIBUTE: return attributes.CustomAttribute(value) + elif name is enums.AttributeType.ALTERNATIVE_NAME: + return self._create_alternative_name(value) else: if not isinstance(name, str): raise ValueError('Unrecognized attribute type: ' @@ -200,6 +202,8 @@ def create_attribute_value_by_enum(self, enum, value): return primitives.Boolean(value, enums.Tags.SENSITIVE) elif enum is enums.Tags.CUSTOM_ATTRIBUTE: return attributes.CustomAttribute(value) + elif enum is enums.Tags.ALTERNATIVE_NAME: + return self._create_alternative_name(value) else: raise ValueError("Unrecognized attribute type: {}".format(enum)) @@ -272,6 +276,15 @@ def _create_cryptographic_usage_mask(self, flags): return attributes.CryptographicUsageMask(mask) + def _create_alternative_name(self, info): + if info: + return attributes.AlternativeName( + alternative_name_value=info.get('alternative_name_value'), + alternative_name_type=info.get('alternative_name_type') + ) + else: + return attributes.AlternativeName() + def _create_application_specific_information(self, info): if info: return attributes.ApplicationSpecificInformation( diff --git a/kmip/demos/pie/locate.py b/kmip/demos/pie/locate.py index 549056e7..7aedce11 100644 --- a/kmip/demos/pie/locate.py +++ b/kmip/demos/pie/locate.py @@ -63,7 +63,7 @@ for initial_date in initial_dates: try: t = time.strptime(initial_date) - except ValueError, TypeError: + except (ValueError, TypeError): logger.error( "Invalid initial date provided: {}".format(initial_date) ) diff --git a/kmip/pie/objects.py b/kmip/pie/objects.py index 7f20b2c8..c35e31a0 100644 --- a/kmip/pie/objects.py +++ b/kmip/pie/objects.py @@ -101,6 +101,23 @@ class ManagedObject(sql.Base): order_by="ManagedObjectName.id" ) names = association_proxy('_names', 'name') + + alternative_names = sqlalchemy.orm.relationship( + "ManagedObjectAlternativeName", + back_populates="mo", + cascade="all, delete-orphan", + order_by="ManagedObjectAlternativeName.id", + passive_deletes=True + ) + + custom_attributes = sqlalchemy.orm.relationship( + "ManagedObjectCustomAttribute", + back_populates="mo", + cascade="all, delete-orphan", + order_by="ManagedObjectCustomAttribute.id", + passive_deletes=True + ) + operation_policy_name = Column( 'operation_policy_name', String(50), diff --git a/kmip/pie/sqltypes.py b/kmip/pie/sqltypes.py index 6b27616a..7d4ff8eb 100644 --- a/kmip/pie/sqltypes.py +++ b/kmip/pie/sqltypes.py @@ -129,7 +129,6 @@ def process_result_value(self, value, dialect): class ManagedObjectName(Base): - __tablename__ = 'managed_object_names' id = Column('id', Integer, primary_key=True) mo_uid = Column('mo_uid', Integer, ForeignKey('managed_objects.uid')) @@ -167,3 +166,78 @@ def __ne__(self, other): return not (self == other) else: return NotImplemented + + +class ManagedObjectAlternativeName(Base): + __tablename__ = 'managed_object_alternative_names' + id = Column('id', Integer, primary_key=True) + mo_uid = Column('mo_uid', Integer, ForeignKey('managed_objects.uid')) + alternative_name_value = Column('alternative_name_value', String) + alternative_name_type = Column( + 'alternative_name_type', + EnumType(enums.AlternativeNameType) + ) + + mo = relationship('ManagedObject', back_populates='alternative_names') + + def __init__(self, alternative_name_value, alternative_name_type): + self.alternative_name_value = alternative_name_value + self.alternative_name_type = alternative_name_type + + def __repr__(self): + return ("" % + (self.alternative_name_value, self.alternative_name_type)) + + def __eq__(self, other): + if isinstance(other, ManagedObjectAlternativeName): + if self.alternative_name_value != other.alternative_name_value: + return False + elif self.alternative_name_type != other.alternative_name_type: + return False + else: + return True + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, ManagedObjectAlternativeName): + return not (self == other) + else: + return NotImplemented + + +class ManagedObjectCustomAttribute(Base): + __tablename__ = 'managed_object_custom_attributes' + id = Column('id', Integer, primary_key=True) + mo_uid = Column('mo_uid', Integer, ForeignKey('managed_objects.uid')) + attribute_name = Column('attribute_name', String) + attribute_text = Column('attribute_text', String) + + mo = relationship('ManagedObject', back_populates='custom_attributes') + + def __init__(self, attribute_name, attribute_text): + self.attribute_name = attribute_name + self.attribute_text = attribute_text + + def __repr__(self): + return ( + "" % (self.attribute_name, self.attribute_text) + ) + + def __eq__(self, other): + if isinstance(other, ManagedObjectCustomAttribute): + if self.attribute_name != other.attribute_name: + return False + elif self.attribute_text != other.attribute_text: + return False + else: + return True + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, ManagedObjectCustomAttribute): + return not (self == other) + else: + return NotImplemented diff --git a/kmip/services/server/engine.py b/kmip/services/server/engine.py index 4c5ff508..7ee81f38 100644 --- a/kmip/services/server/engine.py +++ b/kmip/services/server/engine.py @@ -746,6 +746,22 @@ def _get_attribute_from_managed_object(self, managed_object, attr_name): return None elif attr_name == "Sensitive": return managed_object.sensitive + elif attr_name == 'Alternative Name': + values = [] + for aname in managed_object.alternative_names: + values.append( + { + "alternative_name_value": aname.alternative_name_value, + "alternative_name_type": aname.alternative_name_type + } + ) + return values + elif attr_name.startswith('x-'): + values = [] + for attr in managed_object.custom_attributes: + if attr.attribute_name == attr_name: + values.append(attr.attribute_text) + return values else: # Since custom attribute names are possible, just return None # for unrecognized attributes. This satisfies the spec. @@ -896,6 +912,21 @@ def _set_attribute_on_managed_object(self, managed_object, attribute): managed_object.object_groups.append( objects.ObjectGroup(object_group=value.value) ) + elif attribute_name.startswith("x-"): + for value in attribute_value: + managed_object.custom_attributes.append( + sqltypes.ManagedObjectCustomAttribute( + attribute_name, + value.value) + ) + elif attribute_name == "Alternative Name": + for value in attribute_value: + managed_object.alternative_names.append( + sqltypes.ManagedObjectAlternativeName( + value.alternative_name_value, + value.alternative_name_type + ) + ) else: # TODO (peterhamilton) Remove when all attributes are supported raise exceptions.InvalidField( @@ -2412,6 +2443,18 @@ def _process_locate(self, payload): initial_date, value.value ) + elif name == "Alternative Name": + dval = { + "alternative_name_value": value.alternative_name_value, + "alternative_name_type": value.alternative_name_type + } + if dval not in attribute: + add_object = False + break + elif name.startswith('x-'): + if value.value not in attribute: + add_object = False + break else: if value != attribute: add_object = False diff --git a/kmip/services/server/policy.py b/kmip/services/server/policy.py index 400ffdfd..9008936e 100644 --- a/kmip/services/server/policy.py +++ b/kmip/services/server/policy.py @@ -101,6 +101,31 @@ def __init__(self, self.version_deprecated = version_deprecated +class CustomAttrDict: + """ + Dictionary that translates 'x-' keys into 'Custom Attribute'. + + It is used to support custom attributes for KMIP 1.x. + """ + def __init__(self, dct): + self._dct = dct + + def get(self, key): + if key.startswith('x-'): + return self._dct.get('Custom Attribute') + else: + return self._dct.get(key) + + def keys(self): + return self._dct.keys() + + def __contains__(self, key): + if key.startswith('x-'): + return 'Custom Attribute' in self._dct + else: + return key in self._dct + + class AttributePolicy(object): """ A collection of attribute rules and methods to query those rules. @@ -130,7 +155,7 @@ def __init__(self, version): self._version = version # TODO (peterhamilton) Alphabetize these - self._attribute_rule_sets = { + self._attribute_rule_sets = CustomAttrDict({ 'Unique Identifier': AttributeRuleSet( True, ('server', ), @@ -1078,6 +1103,38 @@ def __init__(self, version): ), contents.ProtocolVersion(1, 0) ), + 'Alternative Name': AttributeRuleSet( + False, + ('server', 'client'), + True, # Only for server-created attributes + True, # Only for client-created attributes + True, # Only for client-created attributes + True, + ( + enums.Operation.CREATE, + enums.Operation.CREATE_KEY_PAIR, + enums.Operation.REGISTER, + enums.Operation.DERIVE_KEY, + enums.Operation.ACTIVATE, + enums.Operation.REVOKE, + enums.Operation.DESTROY, + enums.Operation.CERTIFY, + enums.Operation.RECERTIFY, + enums.Operation.REKEY, + enums.Operation.REKEY_KEY_PAIR + ), + ( + enums.ObjectType.CERTIFICATE, + enums.ObjectType.SYMMETRIC_KEY, + enums.ObjectType.PUBLIC_KEY, + enums.ObjectType.PRIVATE_KEY, + enums.ObjectType.SPLIT_KEY, + enums.ObjectType.TEMPLATE, + enums.ObjectType.SECRET_DATA, + enums.ObjectType.OPAQUE_DATA + ), + contents.ProtocolVersion(1, 0) + ), "Sensitive": AttributeRuleSet( True, ("server", "client"), @@ -1102,7 +1159,7 @@ def __init__(self, version): ), contents.ProtocolVersion(1, 4) ) - } + }) def is_attribute_supported(self, attribute): """ @@ -1115,7 +1172,7 @@ def is_attribute_supported(self, attribute): bool: True if the attribute is supported by the current KMIP version. False otherwise. """ - if attribute not in self._attribute_rule_sets.keys(): + if attribute not in self._attribute_rule_sets: return False rule_set = self._attribute_rule_sets.get(attribute) diff --git a/kmip/tests/unit/services/server/test_policy.py b/kmip/tests/unit/services/server/test_policy.py index fd17e3a3..9835ba8b 100644 --- a/kmip/tests/unit/services/server/test_policy.py +++ b/kmip/tests/unit/services/server/test_policy.py @@ -187,7 +187,8 @@ def test_get_all_attribute_names(self): 'Contact Information', 'Last Change Date', 'Custom Attribute', - "Sensitive" + "Sensitive", + 'Alternative Name' ] result = rules.get_all_attribute_names()