Example #1
0
    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))
Example #2
0
 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))
Example #3
0
    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))
Example #4
0
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
Example #5
0
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
Example #6
0
    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)
Example #8
0
    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))
Example #9
0
 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))
Example #10
0
 def setUpClass(cls):
     # brew_view.load_app(environment="test")
     cls.parser = MongoParser()
Example #11
0
    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))
Example #12
0
    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))
Example #13
0
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")