Separate user response and request schema
There are more then only few attributes only present in the response and cannot be present in the request. The worst effect is that boolean (enabled) cannot be returned as all this amount of values supported on the input. This leads to pretty weird results when using produces openapi spec with the mock generators. Copy out user properties adopting them to the reality of responses. Change-Id: Ic4c81cc8f7b90adb2cb6cea19cf2b3c4bff2c00f Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
This commit is contained in:
1 parent
1d67a0ae78
commit
ee4aef7dd7
2 files changed
+229
-100
No files matched your search
+209
-100
@@ -15,18 +15,18 @@ from typing import Any
|
||||
|
||||
from keystone.api.validation import parameter_types
|
||||
from keystone.api.validation import response_types
|
||||
from keystone.common import validation
|
||||
import keystone.conf
|
||||
from keystone.identity.backends import resource_options as ro
|
||||
from keystone.resource import schema as resource_schema
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
|
||||
|
||||
_identity_name: dict[str, Any] = {
|
||||
'type': 'string',
|
||||
'minLength': 1,
|
||||
'maxLength': 255,
|
||||
'pattern': r'[\S]+',
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 255,
|
||||
"pattern": r"[\S]+",
|
||||
}
|
||||
|
||||
# Schema for Identity v3 API
|
||||
@@ -47,10 +47,10 @@ user_index_request_query: dict[str, Any] = {
|
||||
"password_expires_at": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Filter results based on which user passwords have "
|
||||
"expired. The query should include an operator and "
|
||||
"a timestamp with a colon (:) separating the two, for "
|
||||
"example: `password_expires_at={operator}:{timestamp}`\n"
|
||||
"Filter results based on which user passwords have expired. "
|
||||
"The query should include an operator and a timestamp with a "
|
||||
"colon (:) separating the two, for example: "
|
||||
"`password_expires_at={operator}:{timestamp}`\n"
|
||||
"Valid operators are: lt, lte, gt, gte, eq, and neq\n"
|
||||
" - lt: expiration time lower than the timestamp\n"
|
||||
" - lte: expiration time lower than or equal to the timestamp\n"
|
||||
@@ -61,8 +61,8 @@ user_index_request_query: dict[str, Any] = {
|
||||
"Valid timestamps are of the form: `YYYY-MM-DDTHH:mm:ssZ`."
|
||||
"For example:"
|
||||
"`/v3/users?password_expires_at=lt:2016-12-08T22:02:00Z`\n"
|
||||
"The example would return a list of users whose password "
|
||||
"expired before the timestamp `(2016-12-08T22:02:00Z).`"
|
||||
"The example would return a list of users whose password expired"
|
||||
" before the timestamp `(2016-12-08T22:02:00Z).`"
|
||||
),
|
||||
},
|
||||
"protocol_id": {
|
||||
@@ -87,54 +87,141 @@ user_index_request_query: dict[str, Any] = {
|
||||
}
|
||||
|
||||
_user_properties: dict[str, Any] = {
|
||||
'id': parameter_types.user_id,
|
||||
'default_project_id': validation.nullable(parameter_types.id_string),
|
||||
'description': validation.nullable(parameter_types.description),
|
||||
'domain_id': parameter_types.id_string,
|
||||
'enabled': parameter_types.boolean,
|
||||
'federated': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'idp_id': {'type': 'string'},
|
||||
'protocols': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'protocol_id': {'type': 'string'},
|
||||
'unique_id': {'type': 'string'},
|
||||
"default_project_id": {
|
||||
"type": ["string", "null"],
|
||||
"description": (
|
||||
"The ID of the default project for the user. A user’s default"
|
||||
" project must not be a domain. Setting this attribute does not"
|
||||
" grant any actual authorization on the project, and is merely"
|
||||
" provided for convenience. Therefore, the referenced project does"
|
||||
" not need to exist within the user domain. (Since v3.1) If the"
|
||||
" user does not have authorization to their default project, the"
|
||||
" default project is ignored at token creation. (Since v3.1)"
|
||||
" Additionally, if your default project is not valid, a token is"
|
||||
" issued without an explicit scope of authorization."
|
||||
),
|
||||
},
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The description of the user resource.",
|
||||
},
|
||||
"domain_id": parameter_types.domain_id,
|
||||
"enabled": parameter_types.boolean,
|
||||
"federated": {
|
||||
"description": (
|
||||
"List of federated objects associated with a user. Each object in"
|
||||
" the list contains the idp_id and protocols. protocols is a list"
|
||||
" of objects, each of which contains protocol_id and unique_id of"
|
||||
" the protocol and user respectively."
|
||||
),
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"idp_id": {"type": "string"},
|
||||
"protocols": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocol_id": {"type": "string"},
|
||||
"unique_id": {"type": "string"},
|
||||
},
|
||||
'required': ['protocol_id', 'unique_id'],
|
||||
"required": ["protocol_id", "unique_id"],
|
||||
},
|
||||
'minItems': 1,
|
||||
"minItems": 1,
|
||||
},
|
||||
},
|
||||
'required': ['idp_id', 'protocols'],
|
||||
"required": ["idp_id", "protocols"],
|
||||
},
|
||||
},
|
||||
'links': response_types.links,
|
||||
'name': _identity_name,
|
||||
'password_expires_at': {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"name": {
|
||||
"description": (
|
||||
"The date and time when the password expires. The time zone is UTC. "
|
||||
"This is a response object attribute; not valid for requests. A "
|
||||
"null value indicates that the password never expires."
|
||||
"The user name. Must be unique within the owning domain."
|
||||
),
|
||||
"readOnly": True,
|
||||
**_identity_name,
|
||||
},
|
||||
'options': ro.USER_OPTIONS_REGISTRY.json_schema,
|
||||
"password": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The password for the user.",
|
||||
},
|
||||
"options": ro.USER_OPTIONS_REGISTRY.json_schema,
|
||||
}
|
||||
|
||||
user_schema: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": _user_properties,
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "The user ID."},
|
||||
"default_project_id": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The ID of the default project for the user.",
|
||||
},
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The user description",
|
||||
},
|
||||
"domain_id": resource_schema.domain_id,
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"If the user is enabled, this value is true. If the user is"
|
||||
" disabled, this value is false."
|
||||
),
|
||||
},
|
||||
"federated": {
|
||||
"description": (
|
||||
"List of federated objects associated with a user. Each object"
|
||||
" in the list contains the idp_id and protocols. protocols is"
|
||||
" a list of objects, each of which contains protocol_id and"
|
||||
" unique_id of the protocol and user respectively."
|
||||
),
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"idp_id": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"The Identity Provider ID of the federated user"
|
||||
),
|
||||
},
|
||||
"protocols": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocol_id": {"type": "string"},
|
||||
"unique_id": {"type": "string"},
|
||||
},
|
||||
"required": ["protocol_id", "unique_id"],
|
||||
},
|
||||
"minItems": 1,
|
||||
},
|
||||
},
|
||||
"required": ["idp_id", "protocols"],
|
||||
},
|
||||
},
|
||||
"links": response_types.links,
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"The user name. Must be unique within the owning domain."
|
||||
),
|
||||
},
|
||||
"password_expires_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": (
|
||||
"The date and time when the password expires. The time zone is"
|
||||
" UTC. A null value indicates that the password never expires."
|
||||
),
|
||||
},
|
||||
"options": ro.USER_OPTIONS_REGISTRY.json_schema,
|
||||
},
|
||||
# NOTE(gtema) User resource supports additional attributes which are stored
|
||||
# in the `extra` DB field
|
||||
"additionalProperties": True,
|
||||
"required": ["id", "domain_id", "enabled", "name"],
|
||||
}
|
||||
|
||||
user_index_response_body: dict[str, Any] = {
|
||||
@@ -160,7 +247,7 @@ user_create_request: dict[str, Any] = {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {"type": ["string", "null"]},
|
||||
"domain_id": parameter_types.domain_id,
|
||||
**_user_properties,
|
||||
},
|
||||
"required": ["name"],
|
||||
@@ -175,22 +262,18 @@ user_create_response_body: dict[str, Any] = user_get_response_body
|
||||
|
||||
user_update_properties = copy.deepcopy(_user_properties)
|
||||
# It is not allowed anymore to update domain of the existing user
|
||||
user_update_properties.pop("domain_id", None)
|
||||
user_update_request: dict[str, Any] = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'user': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'password': {'type': ['string', 'null']},
|
||||
**user_update_properties,
|
||||
},
|
||||
'minProperties': 1,
|
||||
'additionalProperties': True,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": _user_properties,
|
||||
"minProperties": 1,
|
||||
"additionalProperties": True,
|
||||
}
|
||||
},
|
||||
'required': ['user'],
|
||||
'additionalProperties': False,
|
||||
"required": ["user"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
user_update_response_body: dict[str, Any] = user_get_response_body
|
||||
@@ -213,18 +296,22 @@ group_index_request_query: dict[str, Any] = {
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
_group_properties: dict[str, Any] = {
|
||||
'description': validation.nullable(parameter_types.description),
|
||||
'domain_id': parameter_types.id_string,
|
||||
'id': {"type": "string", "description": "The user ID.", "readOnly": True},
|
||||
'name': _identity_name,
|
||||
}
|
||||
|
||||
group_schema: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": _group_properties,
|
||||
# NOTE(gtema) Group resource supports additional attributes which are stored
|
||||
# in the `extra` DB field
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The description of the user group resource.",
|
||||
},
|
||||
"domain_id": resource_schema.domain_id,
|
||||
"id": {"type": "string", "description": "The user ID."},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of tje user group.",
|
||||
},
|
||||
},
|
||||
# NOTE(gtema) Group resource supports additional attributes which are
|
||||
# stored in the `extra` DB field
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
@@ -245,19 +332,33 @@ group_index_response_body: dict[str, Any] = {
|
||||
|
||||
group_get_response_body: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"group": group_schema},
|
||||
"required": ["group"],
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"group": group_schema,
|
||||
"required": ["group"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}
|
||||
|
||||
group_create_request_body: dict[str, Any] = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'group': {
|
||||
'type': 'object',
|
||||
'properties': _group_properties,
|
||||
'required': ['name'],
|
||||
'additionalProperties': True,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain_id": parameter_types.domain_id,
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": (
|
||||
"The description of the user group resource."
|
||||
),
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the user group.",
|
||||
**_identity_name,
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": True,
|
||||
}
|
||||
},
|
||||
"required": ["group"],
|
||||
@@ -266,43 +367,51 @@ group_create_request_body: dict[str, Any] = {
|
||||
|
||||
group_create_response_body = group_get_response_body
|
||||
|
||||
group_update_properties = copy.deepcopy(_group_properties)
|
||||
# It is not allowed anymore to update domain of the existing group
|
||||
group_update_properties.pop("domain_id", None)
|
||||
group_update_request_body: dict[str, Any] = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'group': {
|
||||
'type': 'object',
|
||||
'properties': group_update_properties,
|
||||
'minProperties': 1,
|
||||
'additionalProperties': True,
|
||||
group_update_request_body = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"description": (
|
||||
"The new description of the user group resource."
|
||||
),
|
||||
},
|
||||
"name": {
|
||||
"description": "The new name of the user group.",
|
||||
**_identity_name,
|
||||
},
|
||||
},
|
||||
"minProperties": 1,
|
||||
"additionalProperties": True,
|
||||
}
|
||||
},
|
||||
"required": ["group"],
|
||||
'additionalProperties': False,
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
group_update_response_body = group_get_response_body
|
||||
|
||||
_password_change_properties = {
|
||||
'original_password': {'type': 'string'},
|
||||
'password': {'type': 'string'},
|
||||
"original_password": {"type": "string"},
|
||||
"password": {"type": "string"},
|
||||
}
|
||||
if getattr(CONF, 'strict_password_check', None):
|
||||
_password_change_properties['password']['maxLength'] = (
|
||||
if getattr(CONF, "strict_password_check", None):
|
||||
_password_change_properties["password"]["maxLength"] = (
|
||||
CONF.identity.max_password_length
|
||||
)
|
||||
|
||||
if getattr(CONF, 'security_compliance', None):
|
||||
if getattr(CONF.security_compliance, 'password_regex', None):
|
||||
_password_change_properties['password']['pattern'] = (
|
||||
if getattr(CONF, "security_compliance", None):
|
||||
if getattr(CONF.security_compliance, "password_regex", None):
|
||||
_password_change_properties["password"]["pattern"] = (
|
||||
CONF.security_compliance.password_regex
|
||||
)
|
||||
|
||||
password_change = {
|
||||
'type': 'object',
|
||||
'properties': _password_change_properties,
|
||||
'required': ['original_password', 'password'],
|
||||
'additionalProperties': False,
|
||||
"type": "object",
|
||||
"properties": _password_change_properties,
|
||||
"required": ["original_password", "password"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
@@ -19,6 +19,26 @@ from keystone.common import validation
|
||||
from keystone.common.validation import parameter_types as old_parameter_types
|
||||
from keystone.resource.backends import resource_options as ro
|
||||
|
||||
domain_id: dict[str, Any] = {
|
||||
"type": "string",
|
||||
"description": "The ID of the domain.",
|
||||
}
|
||||
|
||||
domain_name: dict[str, Any] = {
|
||||
"type": "string",
|
||||
"description": "The name of the domain.",
|
||||
}
|
||||
|
||||
project_id: dict[str, Any] = {
|
||||
"type": "string",
|
||||
"description": "The ID of the project.",
|
||||
}
|
||||
|
||||
default_project_id: dict[str, Any] = {
|
||||
"type": ["string", "null"],
|
||||
"description": "The ID of the project.",
|
||||
}
|
||||
|
||||
_name_properties = {
|
||||
'type': 'string',
|
||||
'description': 'The resource name.',
|
||||
|
||||
Reference in new issue
Block a user