diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 261d77d5..05c7c671 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Brewtils Changelog ================== +3.27.0 +------ +TBD + +- New Models for User, UserToken, Role, and AliasUserMap +- Must upgrade to a minimum version of Beer Garden 3.27.0 to support new authentication models. If authentication is not enabled, upgrade + is not required. +- Removed 2.0 Legacy support for Principle and LegacyRole models +- Fixed bug in SystemClient to properly assign requester field from parent request + 3.26.4 ------ 7/12/24 diff --git a/brewtils/auto_decorator.py b/brewtils/auto_decorator.py index 0ddd3049..ef3742b0 100644 --- a/brewtils/auto_decorator.py +++ b/brewtils/auto_decorator.py @@ -1,7 +1,6 @@ from inspect import Parameter as InspectParameter # noqa from inspect import signature - from brewtils.models import Command, Parameter diff --git a/brewtils/models.py b/brewtils/models.py index fa594a49..be5b4fab 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -21,9 +21,7 @@ "Event", "Events", "Queue", - "Principal", - "LegacyRole", - "RefreshToken", + "UserToken", "Job", "RequestFile", "File", @@ -37,6 +35,8 @@ "Garden", "Operation", "Resolvable", + "Role", + "User", "Subscriber", "Topic", ] @@ -92,7 +92,7 @@ class Events(Enum): USER_UPDATED = 44 USERS_IMPORTED = 45 ROLE_UPDATED = 46 - ROLES_IMPORTED = 47 + ROLE_DELETED = 47 COMMAND_PUBLISHING_BLOCKLIST_SYNC = 48 COMMAND_PUBLISHING_BLOCKLIST_REMOVE = 49 COMMAND_PUBLISHING_BLOCKLIST_UPDATE = 50 @@ -103,6 +103,13 @@ class Events(Enum): # Next: 57 +class Permissions(Enum): + READ_ONLY = 1 + OPERATOR = 2 + PLUGIN_ADMIN = 3 + GARDEN_ADMIN = 4 + + class BaseModel(object): schema = None @@ -1110,81 +1117,32 @@ def __repr__(self): return "" % (self.name, self.size) -class Principal(BaseModel): - schema = "PrincipalSchema" +class UserToken(BaseModel): + schema = "UserTokenSchema" def __init__( self, id=None, # noqa # shadows built-in + uuid=None, + issued_at=None, + expires_at=None, username=None, - roles=None, - permissions=None, - preferences=None, - metadata=None, ): self.id = id + self.uuid = uuid + self.issued_at = issued_at + self.expires_at = expires_at self.username = username - self.roles = roles - self.permissions = permissions - self.preferences = preferences - self.metadata = metadata def __str__(self): return "%s" % self.username def __repr__(self): - return "" % ( + return "" % ( + self.uuid, + self.issued_at, + self.expires_at, self.username, - self.roles, - self.permissions, - ) - - -class LegacyRole(BaseModel): - schema = "LegacyRoleSchema" - - def __init__( - self, - id=None, # noqa # shadows built-in - name=None, - description=None, - permissions=None, - ): - self.id = id - self.name = name - self.description = description - self.permissions = permissions - - def __str__(self): - return "%s" % self.name - - def __repr__(self): - return "" % (self.name, self.permissions) - - -class RefreshToken(BaseModel): - schema = "RefreshTokenSchema" - - def __init__( - self, - id=None, # noqa # shadows built-in - issued=None, - expires=None, - payload=None, - ): - self.id = id - self.issued = issued - self.expires = expires - self.payload = payload or {} - - def __str__(self): - return "%s" % self.payload - - def __repr__(self): - return "" % ( - self.issued, - self.expires, - self.payload, ) @@ -1471,6 +1429,8 @@ def __init__( parent=None, children=None, metadata=None, + default_user=None, + shared_users=None, ): self.id = id self.name = name @@ -1488,6 +1448,9 @@ def __init__( self.children = children self.metadata = metadata or {} + self.default_user = default_user + self.shared_users = shared_users + def __str__(self): return "%s" % self.name @@ -1658,6 +1621,150 @@ def __repr__(self): ) +class User(BaseModel): + schema = "UserSchema" + + def __init__( + self, + username=None, + id=None, + password=None, + roles=None, + local_roles=None, + upstream_roles=None, + user_alias_mapping=None, + metadata=None, + is_remote=False, + protected=False, + file_generated=False, + ): + self.username = username + self.id = id + self.password = password + self.roles = roles or [] + self.local_roles = local_roles or [] + self.upstream_roles = upstream_roles or [] + self.is_remote = is_remote + self.user_alias_mapping = user_alias_mapping or [] + self.metadata = metadata or {} + self.protected = protected + self.file_generated = file_generated + + def __str__(self): + return "%s: %s" % (self.username, self.roles) + + def __repr__(self): + return "" % ( + self.username, + self.roles, + ) + + def __eq__(self, other): + if not isinstance(other, User): + # don't attempt to compare against unrelated types + return NotImplemented + + return ( + self.username == other.username + and self.roles == other.roles + and self.upstream_roles == other.upstream_roles + and self.is_remote == other.is_remote + and self.user_alias_mapping == other.user_alias_mapping + and self.protected == other.protected + and self.file_generated == other.file_generated + ) + + +class Role(BaseModel): + schema = "RoleSchema" + + # TODO: REMOVE after DB model Updated with Permissions enum + PERMISSION_TYPES = { + "GARDEN_ADMIN", + "PLUGIN_ADMIN", + "OPERATOR", + "READ_ONLY", # Default value if not role is provided + } + + def __init__( + self, + name, + permission=None, + description=None, + id=None, + scope_gardens=None, + scope_namespaces=None, + scope_systems=None, + scope_instances=None, + scope_versions=None, + scope_commands=None, + protected=False, + file_generated=False, + ): + self.name = name + self.permission = permission or Permissions.READ_ONLY.name + self.description = description + self.id = id + self.scope_gardens = scope_gardens or [] + self.scope_namespaces = scope_namespaces or [] + self.scope_systems = scope_systems or [] + self.scope_instances = scope_instances or [] + self.scope_versions = scope_versions or [] + self.scope_commands = scope_commands or [] + self.protected = protected + self.file_generated = file_generated + + def __str__(self): + return "%s" % (self.name) + + def __repr__(self): + return ( + "" + ) % ( + self.id, + self.name, + self.description, + self.permission, + self.scope_gardens, + self.scope_namespaces, + self.scope_systems, + self.scope_instances, + self.scope_versions, + self.scope_commands, + ) + + def __eq__(self, other): + if not isinstance(other, Role): + # don't attempt to compare against unrelated types + return NotImplemented + + return ( + self.name == other.name + and self.description == other.description + and self.permission == other.permission + and self.scope_gardens == other.scope_gardens + and self.scope_namespaces == other.scope_namespaces + and self.scope_systems == other.scope_systems + and self.scope_instances == other.scope_instances + and self.scope_versions == other.scope_versions + and self.scope_commands == other.scope_commands + ) + + +class UpstreamRole(Role): + schema = "UpstreamRoleSchema" + + +class AliasUserMap(BaseModel): + schema = "AliasUserMapSchema" + + def __init__(self, target_garden, username): + self.target_garden = target_garden + self.username = username + + class Subscriber(BaseModel): schema = "SubscriberSchema" diff --git a/brewtils/request_handling.py b/brewtils/request_handling.py index 259f2c0b..fad8e647 100644 --- a/brewtils/request_handling.py +++ b/brewtils/request_handling.py @@ -10,8 +10,8 @@ import six from requests import ConnectionError as RequestsConnectionError -from brewtils.decorators import _parse_method import brewtils.plugin +from brewtils.decorators import _parse_method from brewtils.errors import ( BGGivesUpError, DiscardMessageException, @@ -61,6 +61,7 @@ def process_command(self, request): if parent_request: request.parent = Request(id=str(parent_request.id)) + request.requester = parent_request.requester request.has_parent = True # check for kwargs on the target command diff --git a/brewtils/rest/client.py b/brewtils/rest/client.py index 39b530bc..ad7cfacc 100644 --- a/brewtils/rest/client.py +++ b/brewtils/rest/client.py @@ -23,12 +23,19 @@ def enable_auth(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): + + # Load Token initially if authentication settings are provided + if not self.session.headers.get("Authorization") and ( + (self.username and self.password) or self.client_cert + ): + self.get_tokens() + original_response = method(self, *args, **kwargs) if original_response.status_code != 401: return original_response - # Try to use credentials + # Refresh Token if expired and caused 401 if (self.username and self.password) or self.client_cert: credential_response = self.get_tokens() diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index be5234d9..704ec7ed 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1089,9 +1089,7 @@ def forward(self, operation, **kwargs): SchemaParser.serialize_operation(operation), **kwargs ) - @wrap_response( - parse_method="parse_principal", parse_many=False, default_exc=FetchError - ) + @wrap_response(parse_method="parse_user", parse_many=False, default_exc=FetchError) def get_user(self, user_identifier): """Find a user @@ -1099,7 +1097,7 @@ def get_user(self, user_identifier): user_identifier (str): User ID or username Returns: - Principal: The User + User: The User """ return self.client.get_user(user_identifier) @@ -1108,7 +1106,7 @@ def who_am_i(self): """Find user using the current set of credentials Returns: - Principal: The User + User: The User """ return self.get_user(self.client.username or "anonymous") diff --git a/brewtils/rest/system_client.py b/brewtils/rest/system_client.py index ab681992..bfcd216b 100644 --- a/brewtils/rest/system_client.py +++ b/brewtils/rest/system_client.py @@ -547,6 +547,7 @@ def _wait_for_request(self, request, raise_on_error, timeout): brewtils.plugin.request_context, "current_request", None ) request.has_parent = request.parent is not None + ec = EasyClient( bg_host=brewtils.plugin.CONFIG.bg_host, bg_port=brewtils.plugin.CONFIG.bg_port, @@ -595,6 +596,13 @@ def _construct_bg_request(self, **kwargs): topic = kwargs.pop("_topic", None) propagate = kwargs.pop("_propagate", None) + if parent: + requester = getattr( + brewtils.plugin.request_context.current_request, "requester", None + ) + else: + requester = None + if system_display: metadata["system_display_name"] = system_display if publish: @@ -626,6 +634,7 @@ def _construct_bg_request(self, **kwargs): parent=parent, metadata=metadata, parameters=kwargs, + requester=requester, ) request.parameters = self._resolve_parameters(command, request) diff --git a/brewtils/schema_parser.py b/brewtils/schema_parser.py index cdbc21f4..abe523aa 100644 --- a/brewtils/schema_parser.py +++ b/brewtils/schema_parser.py @@ -37,19 +37,21 @@ class SchemaParser(object): "QueueSchema": brewtils.models.Queue, "ParameterSchema": brewtils.models.Parameter, "PatchSchema": brewtils.models.PatchOperation, - "PrincipalSchema": brewtils.models.Principal, - "RefreshTokenSchema": brewtils.models.RefreshToken, + "UserTokenSchema": brewtils.models.UserToken, "RequestSchema": brewtils.models.Request, "RequestFileSchema": brewtils.models.RequestFile, "FileSchema": brewtils.models.File, "FileChunkSchema": brewtils.models.FileChunk, "FileStatusSchema": brewtils.models.FileStatus, "RequestTemplateSchema": brewtils.models.RequestTemplate, - "LegacyRoleSchema": brewtils.models.LegacyRole, "SystemSchema": brewtils.models.System, "OperationSchema": brewtils.models.Operation, "RunnerSchema": brewtils.models.Runner, "ResolvableSchema": brewtils.models.Resolvable, + "RoleSchema": brewtils.models.Role, + "UpstreamRoleSchema": brewtils.models.UpstreamRole, + "UserSchema": brewtils.models.User, + "AliasUserMapSchema": brewtils.models.AliasUserMap, "SubscriberSchema": brewtils.models.Subscriber, "TopicSchema": brewtils.models.Topic, } @@ -255,25 +257,37 @@ def parse_queue(cls, queue, from_string=False, **kwargs): ) @classmethod - def parse_principal(cls, principal, from_string=False, **kwargs): - """Convert raw JSON string or dictionary to a principal model object + def parse_user(cls, user, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a user model object Args: - principal: The raw input + user: The raw input from_string: True if input is a JSON string, False if a dictionary **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) Returns: - A Principal object + A User object """ - return cls.parse( - principal, brewtils.models.Principal, from_string=from_string, **kwargs - ) + return cls.parse(user, brewtils.models.User, from_string=from_string, **kwargs) @classmethod def parse_role(cls, role, from_string=False, **kwargs): """Convert raw JSON string or dictionary to a role model object + Args: + role: The raw input + from_string: True if input is a JSON string, False if a dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + A Role object + """ + return cls.parse(role, brewtils.models.Role, from_string=from_string, **kwargs) + + @classmethod + def parse_upstream_role(cls, role, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a role model object + Args: role: The raw input from_string: True if input is a JSON string, False if a dictionary @@ -283,28 +297,44 @@ def parse_role(cls, role, from_string=False, **kwargs): A Role object """ return cls.parse( - role, brewtils.models.LegacyRole, from_string=from_string, **kwargs + role, brewtils.models.UpstreamRole, from_string=from_string, **kwargs ) @classmethod - def parse_refresh_token(cls, refresh_token, from_string=False, **kwargs): - """Convert raw JSON string or dictionary to a refresh token object + def parse_alias_user_map(cls, alias_user_map, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a AliasUserMap model object Args: - refresh_token: The raw input + role: The raw input from_string: True if input is a JSON string, False if a dictionary **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) Returns: - A RefreshToken object + A AliasUserMap object """ return cls.parse( - refresh_token, - brewtils.models.RefreshToken, + alias_user_map, + brewtils.models.AliasUserMap, from_string=from_string, **kwargs ) + @classmethod + def parse_user_token(cls, user_token, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a user token object + + Args: + user_token: The raw input + from_string: True if input is a JSON string, False if a dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + A UserToken object + """ + return cls.parse( + user_token, brewtils.models.UserToken, from_string=from_string, **kwargs + ) + @classmethod def parse_job(cls, job, from_string=False, **kwargs): """Convert raw JSON string or dictionary to a job model object @@ -708,11 +738,11 @@ def serialize_queue(cls, queue, to_string=True, **kwargs): ) @classmethod - def serialize_principal(cls, principal, to_string=True, **kwargs): - """Convert a principal model into serialized form + def serialize_user(cls, user, to_string=True, **kwargs): + """Convert a user model into serialized form Args: - principal: The principal object(s) to be serialized + user: The user object(s) to be serialized to_string: True to generate a JSON-formatted string, False to generate a dictionary **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) @@ -721,16 +751,30 @@ def serialize_principal(cls, principal, to_string=True, **kwargs): Serialized representation """ return cls.serialize( - principal, - to_string=to_string, - schema_name=brewtils.models.Principal.schema, - **kwargs + user, to_string=to_string, schema_name=brewtils.models.User.schema, **kwargs ) @classmethod def serialize_role(cls, role, to_string=True, **kwargs): """Convert a role model into serialized form + Args: + role: The role object(s) to be serialized + to_string: True to generate a JSON-formatted string, False to generate a + dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + Serialized representation + """ + return cls.serialize( + role, to_string=to_string, schema_name=brewtils.models.Role.schema, **kwargs + ) + + @classmethod + def serialize_upstream_role(cls, role, to_string=True, **kwargs): + """Convert a role model into serialized form + Args: role: The role object(s) to be serialized to_string: True to generate a JSON-formatted string, False to generate a @@ -743,16 +787,36 @@ def serialize_role(cls, role, to_string=True, **kwargs): return cls.serialize( role, to_string=to_string, - schema_name=brewtils.models.LegacyRole.schema, + schema_name=brewtils.models.UpstreamRole.schema, + **kwargs + ) + + @classmethod + def serialize_alias_user_map(cls, alias_user_map, to_string=True, **kwargs): + """Convert a AliasUserMap model into serialized form + + Args: + AliasUserMap: The AliasUserMap object(s) to be serialized + to_string: True to generate a JSON-formatted string, False to generate a + dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + Serialized representation + """ + return cls.serialize( + alias_user_map, + to_string=to_string, + schema_name=brewtils.models.AliasUserMap.schema, **kwargs ) @classmethod - def serialize_refresh_token(cls, refresh_token, to_string=True, **kwargs): + def serialize_user_token(cls, user_token, to_string=True, **kwargs): """Convert a role model into serialized form Args: - refresh_token: The token object(s) to be serialized + user_token: The token object(s) to be serialized to_string: True to generate a JSON-formatted string, False to generate a dictionary **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) @@ -761,9 +825,9 @@ def serialize_refresh_token(cls, refresh_token, to_string=True, **kwargs): Serialized representation """ return cls.serialize( - refresh_token, + user_token, to_string=to_string, - schema_name=brewtils.models.RefreshToken.schema, + schema_name=brewtils.models.UserToken.schema, **kwargs ) diff --git a/brewtils/schemas.py b/brewtils/schemas.py index 92427abd..2a54bbc7 100644 --- a/brewtils/schemas.py +++ b/brewtils/schemas.py @@ -24,9 +24,7 @@ "LoggingConfigSchema", "EventSchema", "QueueSchema", - "PrincipalSchema", - "LegacyRoleSchema", - "RefreshTokenSchema", + "UserTokenSchema", "JobSchema", "JobExportSchema", "JobExportInputSchema", @@ -39,13 +37,8 @@ "GardenSchema", "OperationSchema", "UserSchema", - "UserCreateSchema", - "UserListSchema", "RoleSchema", - "RoleAssignmentSchema", - "RoleAssignmentDomainSchema", - "GardenDomainIdentifierSchema", - "SystemDomainIdentifierSchema", + "AliasUserMapSchema", "SubscriberSchema", "TopicSchema", ] @@ -76,26 +69,6 @@ def _deserialize_model(_, data, type_field=None, allowed_types=None): return model_schema_map.get(data[type_field])() -def _domain_identifier_schema_selector(_, role_assignment_domain): - scope_schema_map = { - "Garden": GardenDomainIdentifierSchema, - "System": SystemDomainIdentifierSchema, - "Global": Schema, - } - - if isinstance(role_assignment_domain, dict): - scope = role_assignment_domain.get("scope") - else: - scope = role_assignment_domain.scope - - schema = scope_schema_map.get(scope) - - if schema is None: - raise TypeError("Invalid scope: %s" % scope) - - return schema() - - class ModelField(PolyField): """Field representing a Brewtils model @@ -428,28 +401,12 @@ class QueueSchema(BaseSchema): size = fields.Integer(allow_none=True) -class PrincipalSchema(BaseSchema): +class UserTokenSchema(BaseSchema): id = fields.Str(allow_none=True) + uuid = fields.Str(allow_none=True) + issued_at = DateTime(allow_none=True, format="epoch", example="1500065932000") + expires_at = DateTime(allow_none=True, format="epoch", example="1500065932000") username = fields.Str(allow_none=True) - roles = fields.Nested("LegacyRoleSchema", many=True, allow_none=True) - permissions = fields.List(fields.Str(), allow_none=True) - preferences = fields.Dict(allow_none=True) - metadata = fields.Dict(allow_none=True) - - -class LegacyRoleSchema(BaseSchema): - id = fields.Str(allow_none=True) - name = fields.Str(allow_none=True) - description = fields.Str(allow_none=True) - roles = fields.Nested("self", many=True, allow_none=True) - permissions = fields.List(fields.Str(), allow_none=True) - - -class RefreshTokenSchema(BaseSchema): - id = fields.Str(allow_none=True) - issued = DateTime(allow_none=True, format="epoch", example="1500065932000") - expires = DateTime(allow_none=True, format="epoch", example="1500065932000") - payload = fields.Dict(allow_none=True) class DateTriggerSchema(BaseSchema): @@ -519,10 +476,8 @@ class GardenSchema(BaseSchema): "self", exclude=("parent"), many=True, default=None, allow_none=True ) metadata = fields.Dict(allow_none=True) - - -class GardenDomainIdentifierSchema(BaseSchema): - name = fields.Str(required=True) + default_user = fields.Str(allow_none=True) + shared_users = fields.Bool(allow_none=True) class JobSchema(BaseSchema): @@ -606,24 +561,27 @@ class ResolvableSchema(BaseSchema): class RoleSchema(BaseSchema): - id = fields.Str() - name = fields.Str() + permission = fields.Str() description = fields.Str(allow_none=True) - permissions = fields.List(fields.Str()) + id = fields.Str(allow_none=True) + name = fields.Str() + scope_gardens = fields.List(fields.Str(), allow_none=True) + scope_namespaces = fields.List(fields.Str(), allow_none=True) + scope_systems = fields.List(fields.Str(), allow_none=True) + scope_instances = fields.List(fields.Str(), allow_none=True) + scope_versions = fields.List(fields.Str(), allow_none=True) + scope_commands = fields.List(fields.Str(), allow_none=True) + protected = fields.Boolean(allow_none=True) + file_generated = fields.Boolean(allow_none=True) -class RoleAssignmentDomainSchema(BaseSchema): - scope = fields.Str() - identifiers = PolyField( - serialization_schema_selector=_domain_identifier_schema_selector, - deserialization_schema_selector=_domain_identifier_schema_selector, - required=False, - ) +class UpstreamRoleSchema(RoleSchema): + pass -class RoleAssignmentSchema(BaseSchema): - domain = fields.Nested(RoleAssignmentDomainSchema, required=True) - role = fields.Nested(RoleSchema()) +class AliasUserMapSchema(BaseSchema): + target_garden = fields.Str() + username = fields.Str() class SubscriberSchema(BaseSchema): @@ -643,19 +601,17 @@ class TopicSchema(BaseSchema): class UserSchema(BaseSchema): - id = fields.Str() - username = fields.Str() - role_assignments = fields.List(fields.Nested(RoleAssignmentSchema())) - permissions = fields.Dict() - - -class UserCreateSchema(BaseSchema): - username = fields.Str(required=True) - password = fields.Str(required=True, load_only=True) - - -class UserListSchema(BaseSchema): - users = fields.List(fields.Nested(UserSchema())) + id = fields.Str(allow_none=True) + username = fields.Str(allow_none=True) + password = fields.Str(allow_none=True) + roles = fields.List(fields.Str(), allow_none=True) + local_roles = fields.List(fields.Nested(RoleSchema()), allow_none=True) + upstream_roles = fields.List(fields.Nested(UpstreamRoleSchema()), allow_none=True) + user_alias_mapping = fields.List(fields.Nested(AliasUserMapSchema())) + is_remote = fields.Boolean(allow_none=True) + metadata = fields.Dict(allow_none=True) + protected = fields.Boolean(allow_none=True) + file_generated = fields.Boolean(allow_none=True) model_schema_map.update( @@ -676,19 +632,21 @@ class UserListSchema(BaseSchema): "Queue": QueueSchema, "Parameter": ParameterSchema, "PatchOperation": PatchSchema, - "Principal": PrincipalSchema, - "RefreshToken": RefreshTokenSchema, + "UserToken": UserTokenSchema, "Request": RequestSchema, "RequestFile": RequestFileSchema, "File": FileSchema, "FileChunk": FileChunkSchema, "FileStatus": FileStatusSchema, "RequestTemplate": RequestTemplateSchema, - "LegacyRole": LegacyRoleSchema, "System": SystemSchema, "Operation": OperationSchema, "Runner": RunnerSchema, "Resolvable": ResolvableSchema, + "Role": RoleSchema, + "UpstreamRole": UpstreamRoleSchema, + "User": UserSchema, + "AliasUserMap": AliasUserMapSchema, "Subscriber": SubscriberSchema, "Topic": TopicSchema, # Compatibility for the Job trigger types diff --git a/brewtils/test/comparable.py b/brewtils/test/comparable.py index 1dda4a73..c2fc68de 100644 --- a/brewtils/test/comparable.py +++ b/brewtils/test/comparable.py @@ -14,6 +14,7 @@ import brewtils.test from brewtils.models import ( + AliasUserMap, Choices, Command, Connection, @@ -24,21 +25,23 @@ Instance, IntervalTrigger, Job, - LegacyRole, LoggingConfig, Operation, Parameter, PatchOperation, - Principal, Queue, Request, RequestFile, RequestTemplate, Resolvable, + Role, Runner, - System, Subscriber, + System, Topic, + UpstreamRole, + User, + UserToken, ) __all__ = [ @@ -52,7 +55,9 @@ "assert_trigger_equal", "assert_command_equal", "assert_parameter_equal", - "assert_principal_equal", + "assert_user_token_equal", + "assert_user_equal", + "assert_alias_user_map_equal", "assert_request_equal", "assert_role_equal", "assert_system_equal", @@ -197,6 +202,7 @@ def _assert_wrapper(obj1, obj2, expected_type=None, do_raise=False, **kwargs): assert_runner_equal = partial(_assert_wrapper, expected_type=Runner) assert_resolvable_equal = partial(_assert_wrapper, expected_type=Resolvable) assert_connection_equal = partial(_assert_wrapper, expected_type=Connection) +assert_alias_user_map_equal = partial(_assert_wrapper, expected_type=AliasUserMap) assert_subscriber_equal = partial(_assert_wrapper, expected_type=Subscriber) @@ -241,12 +247,28 @@ def assert_event_equal(obj1, obj2, do_raise=False): ) -def assert_principal_equal(obj1, obj2, do_raise=False): +def assert_user_equal(obj1, obj2, do_raise=False): + return _assert_wrapper( + obj1, + obj2, + expected_type=User, + deep_fields={ + "local_roles": partial(assert_role_equal, do_raise=True), + "upstream_roles": partial(assert_upstream_role_equal, do_raise=True), + "user_alias_mapping": partial(assert_alias_user_map_equal, do_raise=True), + }, + do_raise=do_raise, + ) + + +def assert_user_token_equal(obj1, obj2, do_raise=False): return _assert_wrapper( obj1, obj2, - expected_type=Principal, - deep_fields={"roles": partial(assert_role_equal, do_raise=True)}, + expected_type=UserToken, + deep_fields={ + "user": partial(assert_user_equal, do_raise=True), + }, do_raise=do_raise, ) @@ -303,8 +325,16 @@ def assert_role_equal(obj1, obj2, do_raise=False): return _assert_wrapper( obj1, obj2, - expected_type=LegacyRole, - deep_fields={"roles": partial(assert_role_equal, do_raise=True)}, + expected_type=Role, + do_raise=do_raise, + ) + + +def assert_upstream_role_equal(obj1, obj2, do_raise=False): + return _assert_wrapper( + obj1, + obj2, + expected_type=UpstreamRole, do_raise=do_raise, ) diff --git a/brewtils/test/fixtures.py b/brewtils/test/fixtures.py index 3996cc8b..40258ff6 100644 --- a/brewtils/test/fixtures.py +++ b/brewtils/test/fixtures.py @@ -7,6 +7,7 @@ import pytz from brewtils.models import ( + AliasUserMap, Choices, Command, Connection, @@ -17,21 +18,23 @@ Instance, IntervalTrigger, Job, - LegacyRole, LoggingConfig, Operation, Parameter, PatchOperation, - Principal, Queue, Request, RequestFile, RequestTemplate, Resolvable, + Role, Runner, Subscriber, System, Topic, + UpstreamRole, + User, + UserToken, ) @@ -531,38 +534,107 @@ def bg_queue(queue_dict): @pytest.fixture -def principal_dict(legacy_role_dict): +def alias_user_map_dict(): return { - "id": "58542eb571afd47ead90d24f", - "username": "admin", - "roles": [legacy_role_dict], - "permissions": ["bg-all"], - "preferences": {"theme": "dark"}, - "metadata": {"foo": "bar"}, + "target_garden": "test", + "username": "user", } @pytest.fixture -def bg_principal(principal_dict, bg_role): - dict_copy = copy.deepcopy(principal_dict) - dict_copy["roles"] = [bg_role] - return Principal(**dict_copy) +def bg_alias_user_map(alias_user_map_dict): + return AliasUserMap(**alias_user_map_dict) @pytest.fixture -def legacy_role_dict(): +def user_token_dict(user_dict, ts_epoch): return { - "id": "58542eb571afd47ead90d26f", - "name": "bg-admin", - "description": "The admin role", - "permissions": ["bg-all"], + "id": "1", + "uuid": "11111111-2222-4444-5555-66666666666", + "issued_at": ts_epoch, + "expires_at": ts_epoch, + "username": "USERNAME", } @pytest.fixture -def bg_role(legacy_role_dict): - dict_copy = copy.deepcopy(legacy_role_dict) - return LegacyRole(**dict_copy) +def bg_user_token(user_token_dict, ts_dt): + dict_copy = copy.deepcopy(user_token_dict) + dict_copy["issued_at"] = ts_dt + dict_copy["expires_at"] = ts_dt + return UserToken(**dict_copy) + + +@pytest.fixture +def role_dict(): + return { + "permission": "PLUGIN_ADMIN", + "description": "PLUGIN ADMIN ROLE", + "id": "1", + "name": "PLUGIN_ADMIN_ROLE", + "scope_gardens": ["FOO"], + "scope_namespaces": [], + "scope_systems": [], + "scope_instances": [], + "scope_versions": [], + "scope_commands": [], + "protected": False, + "file_generated": False, + } + + +@pytest.fixture +def bg_role(role_dict): + return Role(**role_dict) + + +@pytest.fixture +def upstream_role_dict(): + return { + "permission": "PLUGIN_ADMIN", + "description": "PLUGIN ADMIN ROLE", + "id": "1", + "name": "PLUGIN_ADMIN_ROLE", + "scope_gardens": ["FOO"], + "scope_namespaces": [], + "scope_systems": [], + "scope_instances": [], + "scope_versions": [], + "scope_commands": [], + "protected": False, + "file_generated": False, + } + + +@pytest.fixture +def bg_upstream_role(upstream_role_dict): + return UpstreamRole(**upstream_role_dict) + + +@pytest.fixture +def user_dict(role_dict, upstream_role_dict, alias_user_map_dict): + return { + "id": "1", + "username": "USERNAME", + "password": "HASH", + "roles": ["PLUGIN_ADMIN_ROLE"], + "local_roles": [role_dict], + "upstream_roles": [upstream_role_dict], + "user_alias_mapping": [alias_user_map_dict], + "is_remote": False, + "metadata": {}, + "protected": False, + "file_generated": False, + } + + +@pytest.fixture +def bg_user(user_dict, bg_role, bg_upstream_role, bg_alias_user_map): + dict_copy = copy.deepcopy(user_dict) + dict_copy["upstream_roles"] = [bg_upstream_role] + dict_copy["local_roles"] = [bg_role] + dict_copy["user_alias_mapping"] = [bg_alias_user_map] + return User(**dict_copy) @pytest.fixture @@ -818,6 +890,8 @@ def garden_dict(ts_epoch, system_dict, connection_dict, connection_publishing_di "has_parent": False, "children": [], "metadata": {}, + "default_user": None, + "shared_users": True, } diff --git a/test/models_test.py b/test/models_test.py index ec3f40dc..cb1d8b39 100644 --- a/test/models_test.py +++ b/test/models_test.py @@ -13,12 +13,12 @@ LoggingConfig, Parameter, PatchOperation, - Principal, + User, Queue, Request, RequestFile, RequestTemplate, - LegacyRole, + Role, Subscriber, Topic, ) @@ -531,31 +531,36 @@ def test_repr(self, queue): assert repr(queue) == "" -class TestPrincipal(object): +class TestUser(object): @pytest.fixture - def principal(self): - return Principal(username="admin", roles=["bg-admin"], permissions=["bg-all"]) + def user(self): + return User( + username="admin", + roles=["bg-admin"], + upstream_roles=[Role(name="foo", permission="ADMIN")], + ) - def test_str(self, principal): - assert str(principal) == "admin" + def test_str(self, user): + assert str(user) == "admin: ['bg-admin']" - def test_repr(self, principal): - assert ( - repr(principal) - == "" - ) + def test_repr(self, user): + assert repr(user) == "" -class TestLegacyRole(object): +class TestRole(object): @pytest.fixture def role(self): - return LegacyRole(name="bg-admin", permissions=["bg-all"]) + return Role(name="bg-admin", permission="PLUGIN_ADMIN") def test_str(self, role): assert str(role) == "bg-admin" def test_repr(self, role): - assert repr(role) == "" + assert repr(role) == ( + "" + ) class TestDateTrigger(object): diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index f51b7c35..b58e990a 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -515,11 +515,11 @@ def test_forward(client, rest_client, success, bg_operation): assert rest_client.post_forward.called is True -def test_get_user(client, rest_client, success, bg_principal): +def test_get_user(client, rest_client, success, bg_user): rest_client.get_user.return_value = success - client.get_user(bg_principal.username) - rest_client.get_user.assert_called_once_with(bg_principal.username) + client.get_user(bg_user.username) + rest_client.get_user.assert_called_once_with(bg_user.username) class TestWhoAmI(object): diff --git a/test/schema_parser_test.py b/test/schema_parser_test.py index 9246dba2..94fa1cc2 100644 --- a/test/schema_parser_test.py +++ b/test/schema_parser_test.py @@ -5,11 +5,15 @@ import copy -import brewtils.models import pytest +from marshmallow.exceptions import MarshmallowError +from pytest_lazyfixture import lazy_fixture + +import brewtils.models from brewtils.models import System from brewtils.schema_parser import SchemaParser from brewtils.test.comparable import ( + assert_alias_user_map_equal, assert_command_equal, assert_connection_equal, assert_event_equal, @@ -21,7 +25,6 @@ assert_operation_equal, assert_parameter_equal, assert_patch_equal, - assert_principal_equal, assert_queue_equal, assert_request_equal, assert_request_file_equal, @@ -31,9 +34,10 @@ assert_subscriber_equal, assert_system_equal, assert_topic_equal, + assert_upstream_role_equal, + assert_user_equal, + assert_user_token_equal, ) -from marshmallow.exceptions import MarshmallowError -from pytest_lazyfixture import lazy_fixture class TestParse(object): @@ -123,17 +127,35 @@ def test_no_modify(self, system_dict): lazy_fixture("bg_queue"), ), ( - brewtils.models.Principal, - lazy_fixture("principal_dict"), - assert_principal_equal, - lazy_fixture("bg_principal"), + brewtils.models.User, + lazy_fixture("user_dict"), + assert_user_equal, + lazy_fixture("bg_user"), + ), + ( + brewtils.models.UserToken, + lazy_fixture("user_token_dict"), + assert_user_token_equal, + lazy_fixture("bg_user_token"), + ), + ( + brewtils.models.AliasUserMap, + lazy_fixture("alias_user_map_dict"), + assert_alias_user_map_equal, + lazy_fixture("bg_alias_user_map"), ), ( - brewtils.models.LegacyRole, - lazy_fixture("legacy_role_dict"), + brewtils.models.Role, + lazy_fixture("role_dict"), assert_role_equal, lazy_fixture("bg_role"), ), + ( + brewtils.models.UpstreamRole, + lazy_fixture("upstream_role_dict"), + assert_upstream_role_equal, + lazy_fixture("bg_upstream_role"), + ), ( brewtils.models.Job, lazy_fixture("job_dict"), @@ -263,17 +285,35 @@ def test_single_from_string(self): lazy_fixture("bg_queue"), ), ( - "parse_principal", - lazy_fixture("principal_dict"), - assert_principal_equal, - lazy_fixture("bg_principal"), + "parse_user", + lazy_fixture("user_dict"), + assert_user_equal, + lazy_fixture("bg_user"), + ), + ( + "parse_user_token", + lazy_fixture("user_token_dict"), + assert_user_token_equal, + lazy_fixture("bg_user_token"), + ), + ( + "parse_alias_user_map", + lazy_fixture("alias_user_map_dict"), + assert_alias_user_map_equal, + lazy_fixture("bg_alias_user_map"), ), ( "parse_role", - lazy_fixture("legacy_role_dict"), + lazy_fixture("role_dict"), assert_role_equal, lazy_fixture("bg_role"), ), + ( + "parse_upstream_role", + lazy_fixture("upstream_role_dict"), + assert_upstream_role_equal, + lazy_fixture("bg_upstream_role"), + ), ( "parse_job", lazy_fixture("job_dict"), @@ -408,17 +448,35 @@ def test_single_specific_from_string(self): lazy_fixture("bg_queue"), ), ( - brewtils.models.Principal, - lazy_fixture("principal_dict"), - assert_principal_equal, - lazy_fixture("bg_principal"), + brewtils.models.User, + lazy_fixture("user_dict"), + assert_user_equal, + lazy_fixture("bg_user"), ), ( - brewtils.models.LegacyRole, - lazy_fixture("legacy_role_dict"), + brewtils.models.UserToken, + lazy_fixture("user_token_dict"), + assert_user_token_equal, + lazy_fixture("bg_user_token"), + ), + ( + brewtils.models.AliasUserMap, + lazy_fixture("alias_user_map_dict"), + assert_alias_user_map_equal, + lazy_fixture("bg_alias_user_map"), + ), + ( + brewtils.models.Role, + lazy_fixture("role_dict"), assert_role_equal, lazy_fixture("bg_role"), ), + ( + brewtils.models.UpstreamRole, + lazy_fixture("upstream_role_dict"), + assert_upstream_role_equal, + lazy_fixture("bg_upstream_role"), + ), ( brewtils.models.Job, lazy_fixture("job_dict"), @@ -546,17 +604,35 @@ def test_many(self, model, data, assertion, expected): lazy_fixture("bg_queue"), ), ( - "parse_principal", - lazy_fixture("principal_dict"), - assert_principal_equal, - lazy_fixture("bg_principal"), + "parse_user", + lazy_fixture("user_dict"), + assert_user_equal, + lazy_fixture("bg_user"), + ), + ( + "parse_user_token", + lazy_fixture("user_token_dict"), + assert_user_token_equal, + lazy_fixture("bg_user_token"), + ), + ( + "parse_alias_user_map", + lazy_fixture("alias_user_map_dict"), + assert_alias_user_map_equal, + lazy_fixture("bg_alias_user_map"), ), ( "parse_role", - lazy_fixture("legacy_role_dict"), + lazy_fixture("role_dict"), assert_role_equal, lazy_fixture("bg_role"), ), + ( + "parse_upstream_role", + lazy_fixture("upstream_role_dict"), + assert_upstream_role_equal, + lazy_fixture("bg_upstream_role"), + ), ( "parse_job", lazy_fixture("job_dict"), @@ -655,8 +731,11 @@ class TestSerialize(object): (lazy_fixture("bg_logging_config"), lazy_fixture("logging_config_dict")), (lazy_fixture("bg_event"), lazy_fixture("event_dict")), (lazy_fixture("bg_queue"), lazy_fixture("queue_dict")), - (lazy_fixture("bg_principal"), lazy_fixture("principal_dict")), - (lazy_fixture("bg_role"), lazy_fixture("legacy_role_dict")), + (lazy_fixture("bg_user"), lazy_fixture("user_dict")), + (lazy_fixture("bg_user_token"), lazy_fixture("user_token_dict")), + (lazy_fixture("bg_alias_user_map"), lazy_fixture("alias_user_map_dict")), + (lazy_fixture("bg_role"), lazy_fixture("role_dict")), + (lazy_fixture("bg_upstream_role"), lazy_fixture("upstream_role_dict")), (lazy_fixture("bg_job"), lazy_fixture("job_dict")), (lazy_fixture("bg_cron_job"), lazy_fixture("cron_job_dict")), (lazy_fixture("bg_interval_job"), lazy_fixture("interval_job_dict")), @@ -717,14 +796,29 @@ def test_single(self, model, expected): ("serialize_event", lazy_fixture("bg_event"), lazy_fixture("event_dict")), ("serialize_queue", lazy_fixture("bg_queue"), lazy_fixture("queue_dict")), ( - "serialize_principal", - lazy_fixture("bg_principal"), - lazy_fixture("principal_dict"), + "serialize_user", + lazy_fixture("bg_user"), + lazy_fixture("user_dict"), + ), + ( + "serialize_user_token", + lazy_fixture("bg_user_token"), + lazy_fixture("user_token_dict"), + ), + ( + "serialize_alias_user_map", + lazy_fixture("bg_alias_user_map"), + lazy_fixture("alias_user_map_dict"), ), ( "serialize_role", lazy_fixture("bg_role"), - lazy_fixture("legacy_role_dict"), + lazy_fixture("role_dict"), + ), + ( + "serialize_upstream_role", + lazy_fixture("bg_upstream_role"), + lazy_fixture("upstream_role_dict"), ), ("serialize_job", lazy_fixture("bg_job"), lazy_fixture("job_dict")), ( @@ -796,8 +890,11 @@ def test_single_specific(self, method, data, expected): (lazy_fixture("bg_logging_config"), lazy_fixture("logging_config_dict")), (lazy_fixture("bg_event"), lazy_fixture("event_dict")), (lazy_fixture("bg_queue"), lazy_fixture("queue_dict")), - (lazy_fixture("bg_principal"), lazy_fixture("principal_dict")), - (lazy_fixture("bg_role"), lazy_fixture("legacy_role_dict")), + (lazy_fixture("bg_user"), lazy_fixture("user_dict")), + (lazy_fixture("bg_user_token"), lazy_fixture("user_token_dict")), + (lazy_fixture("bg_alias_user_map"), lazy_fixture("alias_user_map_dict")), + (lazy_fixture("bg_role"), lazy_fixture("role_dict")), + (lazy_fixture("bg_upstream_role"), lazy_fixture("upstream_role_dict")), (lazy_fixture("bg_job"), lazy_fixture("job_dict")), (lazy_fixture("bg_cron_job"), lazy_fixture("cron_job_dict")), (lazy_fixture("bg_interval_job"), lazy_fixture("interval_job_dict")), @@ -875,11 +972,26 @@ class TestRoundTrip(object): (brewtils.models.Event, assert_event_equal, lazy_fixture("bg_event")), (brewtils.models.Queue, assert_queue_equal, lazy_fixture("bg_queue")), ( - brewtils.models.Principal, - assert_principal_equal, - lazy_fixture("bg_principal"), + brewtils.models.User, + assert_user_equal, + lazy_fixture("bg_user"), + ), + ( + brewtils.models.UserToken, + assert_user_token_equal, + lazy_fixture("bg_user_token"), + ), + ( + brewtils.models.AliasUserMap, + assert_alias_user_map_equal, + lazy_fixture("bg_alias_user_map"), + ), + (brewtils.models.Role, assert_role_equal, lazy_fixture("bg_role")), + ( + brewtils.models.UpstreamRole, + assert_upstream_role_equal, + lazy_fixture("bg_upstream_role"), ), - (brewtils.models.LegacyRole, assert_role_equal, lazy_fixture("bg_role")), (brewtils.models.Job, assert_job_equal, lazy_fixture("bg_job")), (brewtils.models.Job, assert_job_equal, lazy_fixture("bg_cron_job")), (brewtils.models.Job, assert_job_equal, lazy_fixture("bg_interval_job")), @@ -923,8 +1035,10 @@ def test_parsed_start(self, model, assertion, data): (brewtils.models.LoggingConfig, lazy_fixture("logging_config_dict")), (brewtils.models.Event, lazy_fixture("event_dict")), (brewtils.models.Queue, lazy_fixture("queue_dict")), - (brewtils.models.Principal, lazy_fixture("principal_dict")), - (brewtils.models.LegacyRole, lazy_fixture("legacy_role_dict")), + (brewtils.models.User, lazy_fixture("user_dict")), + (brewtils.models.UserToken, lazy_fixture("user_token_dict")), + (brewtils.models.Role, lazy_fixture("role_dict")), + (brewtils.models.UpstreamRole, lazy_fixture("upstream_role_dict")), (brewtils.models.Job, lazy_fixture("job_dict")), (brewtils.models.Job, lazy_fixture("cron_job_dict")), (brewtils.models.Job, lazy_fixture("interval_job_dict")), diff --git a/test/schema_test.py b/test/schema_test.py index f032780b..da25bd61 100644 --- a/test/schema_test.py +++ b/test/schema_test.py @@ -9,7 +9,6 @@ from brewtils.schemas import ( BaseSchema, DateTime, - RoleAssignmentSchema, SystemSchema, _deserialize_model, _serialize_model, @@ -96,93 +95,3 @@ def test_deserialize_mapping(self): assert len(models) == len( SchemaParser._models ), "Missing mapped schema for deserialization" - - -class TestRoleAssignmentSchema(object): - @pytest.fixture - def schema(self): - yield RoleAssignmentSchema() - - @pytest.fixture - def role_assignment_garden_scope(self): - role = {"name": "myrole", "permissions": ["perm1"]} - domain = {"scope": "Garden", "identifiers": {"name": "mygarden"}} - role_assignment = {"role": role, "domain": domain} - - yield role_assignment - - @pytest.fixture - def role_assignment_system_scope(self): - role = {"name": "myrole", "permissions": ["perm1"]} - domain = { - "scope": "System", - "identifiers": {"name": "mysystem", "namespace": "mygarden"}, - } - role_assignment = {"role": role, "domain": domain} - - yield role_assignment - - @pytest.fixture - def role_assignment_global_scope(self): - role = {"name": "myrole", "permissions": ["perm1"]} - domain = {"scope": "Global"} - role_assignment = {"role": role, "domain": domain} - - yield role_assignment - - def test_role_assignment_domain_schema_can_deserialize_garden_scope( - self, schema, role_assignment_garden_scope - ): - assert ( - schema.load(role_assignment_garden_scope).data - == role_assignment_garden_scope - ) - - def test_role_assignment_domain_schema_can_deserialize_system_scope( - self, schema, role_assignment_system_scope - ): - assert ( - schema.load(role_assignment_system_scope).data - == role_assignment_system_scope - ) - - def test_role_assignment_domain_schema_can_deserialize_global_scope( - self, schema, role_assignment_global_scope - ): - assert ( - schema.load(role_assignment_global_scope).data - == role_assignment_global_scope - ) - - def test_role_assignment_domain_schema_can_serialize( - self, schema, role_assignment_garden_scope - ): - assert ( - schema.dump(role_assignment_garden_scope).data - == role_assignment_garden_scope - ) - - def test_role_assignment_domain_schema_can_serialize_global_scope( - self, schema, role_assignment_global_scope - ): - assert ( - schema.dump(role_assignment_global_scope).data - == role_assignment_global_scope - ) - - def test_role_assignment_domain_schema_validates_identifiers( - self, schema, role_assignment_system_scope - ): - # Remove one of the required identifier fields - del role_assignment_system_scope["domain"]["identifiers"]["namespace"] - - with pytest.raises(ValidationError): - schema.load(role_assignment_system_scope) - - def test_role_assignment_domain_schema_raises_error_on_invalid_scope( - self, schema, role_assignment_system_scope - ): - role_assignment_system_scope["domain"]["scope"] = "Unsupported" - - with pytest.raises(ValidationError): - schema.load(role_assignment_system_scope)