Make sensitive fields be SecretString

Enable sensitive handling of certain fields known to contain sensitive data
(password, passcode, secret, token.id)

Change-Id: Iae8cf9a084b981600fc0b1b94176c0ef4109c5df
This commit is contained in:
Artem Goncharov 2025-03-24 14:35:22 +01:00
parent 51403407d7
commit a6002e661b
5 changed files with 94 additions and 4 deletions

View File

@ -109,6 +109,18 @@ class String(BasePrimitiveType):
return '"foo"'
class SecretString(String):
type_hint: str = "SecretString"
@property
def imports(self) -> set[str]:
return {
"secrecy::SecretString",
"crate::api::common::serialize_sensitive_string",
"crate::api::common::serialize_sensitive_optional_string",
}
class JsonValue(BasePrimitiveType):
type_hint: str = "Value"
builder_macros: set[str] = {"setter(into)"}

View File

@ -63,6 +63,12 @@ class String(common_rust.String):
return set()
class SecretString(String):
"""CLI SecretString"""
pass
class IntString(common.BasePrimitiveType):
"""CLI Integer or String"""
@ -667,6 +673,14 @@ class RequestTypeManager(common_rust.TypeManager):
for field_name, field in type_model.fields.items():
is_nullable: bool = False
field_data_type = self.convert_model(field.data_type)
if (
field_name
in ["password", "original_password", "secret", "passcode"]
or self.get_model_name(type_model.reference) == "Token"
and field_name == "id"
) and isinstance(field_data_type, String):
field_data_type = SecretString(format=field_data_type.format)
if isinstance(field_data_type, self.option_type_class):
# Unwrap Option into "is_nullable"
# NOTE: but perhaps Option<Option> is better (not set vs set

View File

@ -17,6 +17,9 @@ import subprocess
from typing import Type, Any
from codegenerator.base import BaseGenerator
from codegenerator.common import BasePrimitiveType
from codegenerator.common import BaseCombinedType
from codegenerator.common import BaseCompoundType
from codegenerator import common
from codegenerator import model
from codegenerator.common import BaseCompoundType
@ -95,6 +98,13 @@ class StructField(common_rust.StructField):
macros.add(f'rename="{self.remote_name}"')
if self.is_optional:
macros.add('skip_serializing_if = "Option::is_none"')
if isinstance(self.data_type, common_rust.SecretString):
if self.is_optional:
macros.add(
'serialize_with = "serialize_sensitive_optional_string"'
)
else:
macros.add('serialize_with = "serialize_sensitive_string"')
return f"#[serde({', '.join(sorted(macros))})]"
@ -271,6 +281,59 @@ class TypeManager(common_rust.TypeManager):
param.setter_type = "list"
self.parameters[k] = param
def _get_struct_type(self, type_model: model.Struct) -> Struct:
"""Convert model.Struct into Rust `Struct`"""
struct_class = self.data_type_mapping[model.Struct]
mod = struct_class(
name=self.get_model_name(type_model.reference),
description=common_rust.sanitize_rust_docstrings(
type_model.description
),
)
field_class = mod.field_type_class_
for field_name, field in type_model.fields.items():
is_nullable: bool = False
field_data_type = self.convert_model(field.data_type)
if (
field_name
in ["password", "original_password", "secret", "passcode"]
or self.get_model_name(type_model.reference) == "Token"
and field_name == "id"
) and isinstance(field_data_type, String):
field_data_type = common_rust.SecretString(
format=field_data_type.format
)
if isinstance(field_data_type, self.option_type_class):
# Unwrap Option into "is_nullable" NOTE: but perhaps
# Option<Option> is better (not set vs set explicitly to None
# )
is_nullable = True
if isinstance(field_data_type.item_type, common_rust.Array):
# Unwrap Option<Option<Vec...>>
field_data_type = field_data_type.item_type
f = field_class(
local_name=self.get_local_attribute_name(field_name),
remote_name=self.get_remote_attribute_name(field_name),
description=common_rust.sanitize_rust_docstrings(
field.description
),
data_type=field_data_type,
is_optional=not field.is_required,
is_nullable=is_nullable,
)
mod.fields[field_name] = f
if type_model.additional_fields:
definition = type_model.additional_fields
# Structure allows additional fields
if isinstance(definition, bool):
mod.additional_fields_type = self.primitive_type_mapping[
model.PrimitiveAny
]
else:
mod.additional_fields_type = self.convert_model(definition)
return mod
class RustSdkGenerator(BaseGenerator):
def __init__(self):

View File

@ -23,7 +23,8 @@
}
{%- elif v.data_type.format is defined and v.data_type.format == "password" %}
if let Some(val) = &args.{{ v.local_name }} {
{{ builder_name }}.{{ v.remote_name }}(val);
{# val.clone() is necessary due to SecretBox not implementing From<&String> #}
{{ builder_name }}.{{ v.remote_name }}(val.clone());
} else {
let secret = Password::new()
{%- if v.description %}

View File

@ -270,7 +270,7 @@ Some({{ val }})
let {{ builder_name }}: openstack_sdk::api::{{ sdk_mod_path | join("::") }}::{{ param.data_type.name }}Builder = TryFrom::try_from(&{{ val_var}})?;
{{ dst_var }}.{{ param.remote_name }}({{ builder_name }}.build()?);
{%- elif param.data_type.__class__.__name__ == "String" %}
{%- elif param.data_type.__class__.__name__ in ["String", "SecretString"] %}
{%- if is_nullable and not param.is_optional %}
{{ dst_var }}.{{ param.remote_name }}({{ val_var | replace("&", "") }}.clone());
{%- elif is_nullable and param.is_optional %}
@ -385,7 +385,7 @@ Some({{ val }})
{%- elif param.data_type.item_type.__class__.__name__ == "ArrayInput" and param.data_type.item_type.__class__.__name__ == "ArrayInput" %}
{#- Array of Arrays - we should have the SDK setter for that #}
{{ dst_var }}.{{ param.remote_name }}({{ val_var }}.into_iter());
{%- elif param.data_type.item_type.__class__.__name__ == "String" and original_item_type.__class__.__name__ == "StructInput" %}
{%- elif param.data_type.item_type.__class__.__name__ in ["String", "SecretString"] and original_item_type.__class__.__name__ == "StructInput" %}
{#- Single field structure replaced with only string #}
{%- set original_type = param.data_type.item_type.original_data_type %}
{%- set original_field = original_type.fields[param.data_type.item_type.original_data_type.fields.keys()|list|first] %}
@ -398,7 +398,7 @@ Some({{ val }})
)
.collect();
{{ dst_var }}.{{ param.remote_name }}({{ builder_name }});
{%- elif param.data_type.item_type.__class__.__name__ == "String" and original_type.__class__.__name__ == "ArrayInput" %}
{%- elif param.data_type.item_type.__class__.__name__ in ["String", "SecretString"] and original_type.__class__.__name__ == "ArrayInput" %}
{#- Single field structure replaced with only string #}
{{ dst_var }}.{{ param.remote_name }}(
val.iter()