def post(self): """ --- summary: Create a new Role parameters: - name: role in: body description: The Role definition schema: $ref: '#/definitions/Role' consumes: - application/json responses: 201: description: A new Role has been created schema: $ref: '#/definitions/Role' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Roles """ role = MongoParser.parse_role(self.request.decoded_body, from_string=True) # Make sure all new permissions are real if not set(role.permissions).issubset(Permissions.values): invalid = set(role.permissions).difference(Permissions.values) raise ModelValidationError("Permissions %s do not exist" % invalid) # And the same for nested roles nested_roles = [] for nested_role in role.roles: try: db_role = Role.objects.get(name=nested_role.name) # There shouldn't be any way to construct a cycle with a new # role, but check just to be sure ensure_no_cycles(role, db_role) nested_roles.append(db_role) except DoesNotExist: raise ModelValidationError("Role '%s' does not exist" % nested_role.name) role.roles = nested_roles role.save() self.set_status(201) self.write(MongoParser.serialize_role(role, to_string=False))
def get(self, role_id): """ --- summary: Retrieve all specific Role parameters: - name: role_id in: path required: true description: The ID of the Role type: string responses: 200: description: Role with the given ID schema: $ref: '#/definitions/Role' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Roles """ self.write( MongoParser.serialize_role(Role.objects.get(id=str(role_id)), to_string=False))
def get(self): """ --- summary: Retrieve all Users responses: 200: description: All Users schema: type: array items: $ref: '#/definitions/User' 50x: $ref: '#/definitions/50xError' tags: - Users """ principals = Principal.objects.all().select_related(max_depth=1) for principal in principals: principal.permissions = coalesce_permissions(principal.roles)[1] self.set_header("Content-Type", "application/json; charset=UTF-8") self.write( MongoParser.serialize_principal(principals, to_string=True, many=True))
def to_brewtils( obj: Union[MongoModel, List[MongoModel], QuerySet] ) -> Union[ModelItem, List[ModelItem], None]: """Convert an item from its Mongo model to its Brewtils one Args: obj: The Mongo model item or QuerySet Returns: The Brewtils model item """ if obj is None: return obj if isinstance(obj, (list, QuerySet)): if len(obj) == 0: return [] model_class = obj[0].brewtils_model many = True else: model_class = obj.brewtils_model many = False if getattr(obj, "pre_serialize", None): obj.pre_serialize() serialized = MongoParser.serialize(obj, to_string=True) parsed = SchemaParser.parse(serialized, model_class, from_string=True, many=many) return parsed
def from_brewtils(obj: ModelItem) -> MongoModel: """Convert an item from its Brewtils model to its Mongo one. Args: obj: The Brewtils model item Returns: The Mongo model item """ model_dict = SchemaParser.serialize(obj, to_string=False) mongo_obj = MongoParser.parse(model_dict, type(obj), from_string=False) return mongo_obj
def post(self): """ --- summary: Create a new User parameters: - name: user in: body description: The user schema: type: object properties: username: type: string description: the name password: type: string description: the password required: - username - password consumes: - application/json responses: 201: description: A new User has been created schema: $ref: '#/definitions/User' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Users """ parsed = json.loads(self.request.decoded_body) user = Principal( username=parsed["username"], hash=custom_app_context.hash(parsed["password"]), ) if "roles" in parsed: user.roles = [ Role.objects.get(name=name) for name in parsed["roles"] ] user.save() user.permissions = coalesce_permissions(user.roles)[1] self.set_status(201) self.write(MongoParser.serialize_principal(user, to_string=False))
def test_get(self): response = self.app.get("/api/v1/requests/id") self.assertEqual(200, response.status_code) self.objects_mock.get.assert_called_with(id="id") self.objects_mock.assert_called_with(parent=self.default_request) response_request = MongoParser().parse_request(response.data, from_string=True) self.assertEqual(self.default_request.system, response_request.system) self.assertEqual(self.default_request.command, response_request.command) self.assertDictEqual( self.default_request.parameters, response_request.parameters ) self.assertEqual(self.default_request.output, response_request.output) self.assertEqual(self.default_request.status, response_request.status)
def get(self, user_identifier): """ --- summary: Retrieve a specific User parameters: - name: user_identifier in: path required: true description: The ID or name of the User type: string responses: 200: description: User with the given ID schema: $ref: '#/definitions/User' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Users """ if user_identifier == "anonymous": principal = beer_garden.api.http.anonymous_principal else: # Need fine-grained access control here if user_identifier not in [ str(self.current_user.id), self.current_user.username, ]: check_permission(self.current_user, [Permissions.USER_READ]) try: principal = Principal.objects.get(id=str(user_identifier)) except (DoesNotExist, ValidationError): principal = Principal.objects.get( username=str(user_identifier)) principal.permissions = coalesce_permissions(principal.roles)[1] self.write(MongoParser.serialize_principal(principal, to_string=False))
def get(self): """ --- summary: Retrieve all Roles responses: 200: description: All Roles schema: type: array items: $ref: '#/definitions/Role' 50x: $ref: '#/definitions/50xError' tags: - Roles """ self.set_header("Content-Type", "application/json; charset=UTF-8") self.write( MongoParser.serialize_role(Role.objects.all(), many=True, to_string=True))
def setUpClass(cls): # brew_view.load_app(environment="test") cls.parser = MongoParser()
def patch(self, role_id): """ --- summary: Partially update a Role description: | The body of the request needs to contain a set of instructions detailing the updates to apply: ```JSON [ { "operation": "add", "path": "/permissions", "value": "ALL" } ] ``` parameters: - name: role_id in: path required: true description: The ID of the Role type: string - name: patch in: body required: true description: Instructions for how to update the Role schema: $ref: '#/definitions/Patch' responses: 200: description: Role with the given ID schema: $ref: '#/definitions/Role' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Roles """ role = Role.objects.get(id=str(role_id)) operations = MongoParser.parse_patch(self.request.decoded_body, many=True, from_string=True) for op in operations: if op.path == "/permissions": try: if op.operation == "add": role.permissions.append(Permissions(op.value).value) elif op.operation == "remove": role.permissions.remove(Permissions(op.value).value) elif op.operation == "set": role.permissions = [ Permissions(perm).value for perm in op.value ] else: raise ModelValidationError( "Unsupported operation '%s'" % op.operation) except ValueError: raise ModelValidationError( "Permission '%s' does not exist" % op.value) elif op.path == "/roles": try: if op.operation == "add": new_nested = Role.objects.get(name=op.value) ensure_no_cycles(role, new_nested) role.roles.append(new_nested) elif op.operation == "remove": role.roles.remove(Role.objects.get(name=op.value)) elif op.operation == "set": # Do this one at a time to be super sure about cycles role.roles = [] for role_name in op.value: new_role = Role.objects.get(name=role_name) ensure_no_cycles(role, new_role) role.roles.append(new_role) else: raise ModelValidationError( "Unsupported operation '%s'" % op.operation) except DoesNotExist: raise ModelValidationError("Role '%s' does not exist" % op.value) elif op.path == "/description": if op.operation != "update": raise ModelValidationError("Unsupported operation '%s'" % op.operation) role.description = op.value else: raise ModelValidationError("Unsupported path '%s'" % op.path) role.save() # Any modification to roles will possibly modify the anonymous user beer_garden.api.http.anonymous_principal = anonymous_principal() self.write(MongoParser.serialize_role(role, to_string=False))
def patch(self, user_id): """ --- summary: Partially update a User description: | The body of the request needs to contain a set of instructions detailing the updates to apply: ```JSON [ { "operation": "add", "path": "/roles", "value": "admin" } ] ``` parameters: - name: user_id in: path required: true description: The ID of the User type: string - name: patch in: body required: true description: Instructions for how to update the User schema: $ref: '#/definitions/Patch' responses: 200: description: User with the given ID schema: $ref: '#/definitions/User' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Users """ principal = Principal.objects.get(id=str(user_id)) operations = MongoParser.parse_patch(self.request.decoded_body, many=True, from_string=True) # Most things only need a permission check if updating a different user if user_id != str(self.current_user.id): check_permission(self.current_user, [Permissions.USER_UPDATE]) for op in operations: if op.path == "/roles": # Updating roles always requires USER_UPDATE check_permission(self.current_user, [Permissions.USER_UPDATE]) try: if op.operation == "add": principal.roles.append(Role.objects.get(name=op.value)) elif op.operation == "remove": principal.roles.remove(Role.objects.get(name=op.value)) elif op.operation == "set": principal.roles = [ Role.objects.get(name=name) for name in op.value ] else: raise ModelValidationError( "Unsupported operation '%s'" % op.operation) except DoesNotExist: raise ModelValidationError("Role '%s' does not exist" % op.value) elif op.path == "/username": if op.operation == "update": principal.username = op.value else: raise ModelValidationError("Unsupported operation '%s'" % op.operation) elif op.path == "/password": if op.operation != "update": raise ModelValidationError("Unsupported operation '%s'" % op.operation) if isinstance(op.value, dict): current_password = op.value.get("current_password") new_password = op.value.get("new_password") else: current_password = None new_password = op.value if user_id == str(self.current_user.id): if current_password is None: raise ModelValidationError( "In order to update your own password, you must provide " "your current password") if not custom_app_context.verify(current_password, self.current_user.hash): raise RequestForbidden("Invalid password") principal.hash = custom_app_context.hash(new_password) if "changed" in principal.metadata: principal.metadata["changed"] = True elif op.path == "/preferences/theme": if op.operation == "set": principal.preferences["theme"] = op.value else: raise ModelValidationError("Unsupported operation '%s'" % op.operation) else: raise ModelValidationError("Unsupported path '%s'" % op.path) principal.save() principal.permissions = coalesce_permissions(principal.roles)[1] self.write(MongoParser.serialize_principal(principal, to_string=False))
class TokenListAPI(BaseHandler): parser = MongoParser() logger = logging.getLogger(__name__) def __init__(self, *args, **kwargs): super(TokenListAPI, self).__init__(*args, **kwargs) self.executor = ProcessPoolExecutor() def get(self): """ --- summary: Use a refresh token to retrieve a new access token description: | Your refresh token can either be set in a cookie (which we set on your session when you logged in) or you can include the refresh ID as a header named "X-BG-RefreshID" responses: 200: description: New Auth Token schema: $ref: '#/definitions/RefreshToken' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ self.write(json.dumps(self._refresh_token())) def patch(self): """ --- summary: Refresh an auth token. description: | The body of the request needs to contain a set of instructions. Currently the only operation supported is `refresh`, with path `/payload`: ```JSON [ { "operation": "refresh", "path": "/payload", "value": "REFRESH_ID" } ] ``` If you do not know your REFRESH_ID, it should be set in a cookie by the server. If you leave `value` as `null` and include this cookie, then we will automatically refresh. Also, if you are using a cookie, you should really consider just using a GET on /api/v1/tokens as it has the same effect. parameters: - name: patch in: body required: true description: Instructions for what to do schema: $ref: '#/definitions/Patch' responses: 200: description: New Auth token schema: $ref: '#/definitions/RefreshToken' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ operations = self.parser.parse_patch(self.request.decoded_body, many=True, from_string=True) token = None for op in operations: if op.operation == "refresh": if op.path == "/payload": token = self._refresh_token(op.value) else: raise ModelValidationError("Unsupported path '%s'" % op.path) else: raise ModelValidationError("Unsupported operation '%s'" % op.operation) self.write(json.dumps(token)) def delete(self): """ --- summary: Remove a refresh token description: | Your refresh token can either be set in a cookie (which we set on your session when you logged in) or you can include the refresh ID as a header named "X-BG-RefreshID" responses: 204: description: Token has been successfully deleted 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ token = self._get_refresh_token() if token: token.delete() self.clear_cookie(self.REFRESH_COOKIE_NAME) self.set_status(204) return raise HTTPError(status_code=403, log_message="Bad credentials") @coroutine def post(self): """ --- summary: Use credentials to generate access and refresh tokens responses: 200: description: All Tokens schema: type: array items: $ref: '#/definitions/Command' 50x: $ref: '#/definitions/50xError' tags: - Tokens """ parsed_body = json.loads(self.request.decoded_body) try: principal = Principal.objects.get(username=parsed_body["username"]) if (config.get("auth.guest_login_enabled") and principal.username == beer_garden.api.http.anonymous_principal.username): verified = True else: verified = yield self.executor.submit( verify, str(parsed_body["password"]), str(principal.hash)) if verified: tokens = generate_tokens(principal, self.REFRESH_COOKIE_EXP) # This is a semi-done solution. To really do this, we cannot give them # a token, instead we should return an error, indicating they need to # update their password, and then login again. In the short term, this # will be enough. This is really meant only to work for our UI so # backwards compatibility is not a concern. if principal.metadata.get( "auto_change" ) and not principal.metadata.get("changed"): self.set_header("change_password_required", "true") if parsed_body.get("remember_me", False): self.set_secure_cookie( self.REFRESH_COOKIE_NAME, tokens["refresh"], expires_days=self.REFRESH_COOKIE_EXP, ) self.write(json.dumps(tokens)) return except DoesNotExist: # Still attempt to verify something so the request takes a while custom_app_context.verify("", None) raise HTTPError(status_code=403, log_message="Bad credentials") def _get_refresh_token(self, token_id=None): if not token_id: token_id = self.get_refresh_id_from_cookie() if not token_id and self.request.headers: token_id = self.request.headers.get("X-BG-RefreshID", None) if token_id: try: return RefreshToken.objects.get(id=token_id) except DoesNotExist: pass return None def _refresh_token(self, token_id=None): token = self._get_refresh_token(token_id) if token and datetime.utcnow() < token.expires: return {"token": generate_access_token(token.payload)} else: raise HTTPError(status_code=403, log_message="Bad credentials")