Merge "Separate user response and request schema"

This commit is contained in:
Zuul 2025-06-16 09:25:01 +00:00 committed by Gerrit Code Review
commit f8539f7541
2 changed files with 229 additions and 100 deletions

View file

@ -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 users 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,
}

View file

@ -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.',