Exemplo n.º 1
0
    def patch(self, username: str, token: AccessToken.Payload):
        query = {"username": username}
        request_json = request.get_json()
        if username == token.username:
            # user is trying to set their own password

            # get current password
            password_current = request_json.get("current", None)
            if password_current is None:
                raise errors.BadRequest()

            # get current password hash
            user = Users().find_one(query, {"password_hash": 1})
            if user is None:
                raise errors.NotFound()

            # check current password is valid
            is_valid = check_password_hash(user["password_hash"],
                                           password_current)
            if not is_valid:
                raise errors.Unauthorized()
        else:
            # user is trying to set other user's password
            # check permission
            if not token.get_permission("users", "change_password"):
                raise errors.NotEnoughPrivilege()

        # get new password
        password_new = request_json.get("new", None)
        if password_new is None:
            raise errors.BadRequest()

        # set new password
        matched_count = (Users().update_one(
            query, {
                "$set": {
                    "password_hash": generate_password_hash(password_new)
                }
            }).matched_count)

        # send response
        if matched_count == 0:
            raise errors.NotFound()
        return Response(status=HTTPStatus.NO_CONTENT)
Exemplo n.º 2
0
def ssh_key():
    """
    Validate ssh public keys exists and matches with username
    """

    # validate request json
    class KeySchema(Schema):
        username = fields.String(required=True,
                                 validate=validate.Length(min=1))
        key = fields.String(required=True, validate=validate.Length(min=1))

    try:
        request_json = KeySchema().load(request.get_json())
    except ValidationError as e:
        raise errors.InvalidRequestJSON(e.messages)

    # compute fingerprint
    try:
        key = request_json["key"]
        rsa_key = paramiko.RSAKey(data=base64.b64decode(key))
        fingerprint = binascii.hexlify(rsa_key.get_fingerprint()).decode()
    except (binascii.Error, paramiko.SSHException):
        raise errors.BadRequest("Invalid RSA key")

    # database
    username = request_json["username"]
    user = Users().update_one(
        {
            "username": username,
            "ssh_keys": {
                "$elemMatch": {
                    "fingerprint": fingerprint
                }
            },
        },
        {"$set": {
            "ssh_keys.$.last_used": datetime.now()
        }},
    )

    if user.matched_count == 0:
        raise errors.Unauthorized()
    return Response(status=HTTPStatus.NO_CONTENT)
Exemplo n.º 3
0
    def post(self, token: AccessToken.Payload):

        try:
            request_json = UserCreateSchema().load(request.get_json())
        except ValidationError as e:
            raise errors.InvalidRequestJSON(e.messages)

        # generate password hash
        password = request_json.pop("password")
        request_json["password_hash"] = generate_password_hash(password)

        # fetch permissions
        request_json["scope"] = ROLES.get(request_json.pop("role"))

        try:
            user_id = Users().insert_one(request_json).inserted_id
            return jsonify({"_id": user_id})
        except DuplicateKeyError:
            raise errors.BadRequest("User already exists")
Exemplo n.º 4
0
    def patch(self, token: AccessToken.Payload, username: str):

        # find user based on username
        query = {"username": username}
        if Users().count_documents(query) != 1:
            raise errors.NotFound()

        try:
            request_json = UserUpdateSchema().load(request.get_json())
        except ValidationError as e:
            raise errors.BadRequest(e.messages)

        update = {}
        if "email" in request_json:
            update["email"] = request_json["email"]
        if "role" in request_json:
            update["scope"] = ROLES.get(request_json["role"])

        Users().update_one(query, {"$set": update})

        return Response(status=HTTPStatus.NO_CONTENT)
Exemplo n.º 5
0
    def post(self, username: str, token: AccessToken.Payload):
        # if user in url is not user in token, not allowed to add ssh keys
        if username != token.username:
            if not token.get_permission("users", "ssh_keys"):
                raise errors.NotEnoughPrivilege()

        try:
            request_json = KeySchema().load(request.get_json())
        except ValidationError as e:
            raise errors.InvalidRequestJSON(e.messages)

        # parse public key string
        key = request_json["key"]
        key_parts = key.split(" ")
        if len(key_parts) >= 2:
            key = key_parts[1]

        # compute fingerprint
        try:
            rsa_key = paramiko.RSAKey(data=base64.b64decode(key))
            fingerprint = binascii.hexlify(rsa_key.get_fingerprint()).decode()
        except (binascii.Error, paramiko.SSHException):
            raise errors.BadRequest("Invalid RSA key")

        # get existing ssh key fingerprints
        query = {"username": username}
        user = Users().find_one(query, {"ssh_keys.fingerprint": 1})
        if user is None:
            raise errors.NotFound()

        # find out if new ssh already exist
        fingerprints = set(
            [ssh_key["fingerprint"] for ssh_key in user.get("ssh_keys", [])]
        )
        if fingerprint in fingerprints:
            raise errors.BadRequest("SSH key already exists")

        # add new ssh key to database
        ssh_key = {
            "name": request_json["name"],
            # ssh-keygen -l -f xxx.pub -E md5 - just data, without dots
            "fingerprint": fingerprint,
            "key": key,
            "type": "RSA",
            "added": datetime.now(),
            "last_used": None,
        }

        # get PKCS8 - ssh-keygen -e -f xxx.priv -m PKCS8
        with tempfile.NamedTemporaryFile(mode="w", suffix=".pub", delete=False) as fp:
            fp.write("ssh-rsa {} {}\n".format(ssh_key["key"], ssh_key["name"]))
        keygen = subprocess.run(
            ["/usr/bin/ssh-keygen", "-e", "-f", fp.name, "-m", "PKCS8"],
            capture_output=True,
            text=True,
        )
        if keygen.returncode != 0:
            raise errors.BadRequest(keygen.stderr)
        ssh_key.update({"pkcs8_key": keygen.stdout})
        os.unlink(fp.name)

        Users().update_one(query, {"$push": {"ssh_keys": ssh_key}})

        return Response(status=HTTPStatus.CREATED)
Exemplo n.º 6
0
def asymmetric_key_auth():
    """authenticate using signed message and generate tokens

    - message in X-SSHAuth-Message HTTP header
    - base64 signature in X-SSHAuth-Signature HTTP header
    - decode standard message: username:timestamp(UTC ISO)
    - verify timestamp is less than a minute old
    - verify username matches our database
    - verify signature of message with username's public keys
    - generate tokens"""

    # check the message's validity
    try:
        message = request.headers["X-SSHAuth-Message"]
        signature = base64.b64decode(request.headers["X-SSHAuth-Signature"])
        username, timestamp = message.split(":", 1)
        timestamp = datetime.datetime.fromisoformat(timestamp)
    except KeyError as exc:
        raise errors.BadRequest("Missing header for `{}`".format("".join(
            exc.args[:1])))
    except binascii.Error:
        raise errors.BadRequest("Invalid signature format (not base64)")
    except Exception as exc:
        logger.error(f"Invalid message format: {exc}")
        logger.exception(exc)
        raise errors.BadRequest("Invalid message format")

    if (datetime.datetime.utcnow() -
            timestamp).total_seconds() > MESSAGE_VALIDITY:
        raise errors.Unauthorized(
            f"message too old or peers desyncrhonised: {MESSAGE_VALIDITY}s")

    user = Users().find_one({"username": username}, {
        "username": 1,
        "scope": 1,
        "ssh_keys": 1
    })
    if user is None:
        raise errors.Unauthorized("User not found")  # we shall never get there

    ssh_keys = user.pop("ssh_keys", [])

    # check that the message was signed with a known private key
    authenticated = False
    with tempfile.TemporaryDirectory() as tmp_dirname:
        tmp_dir = pathlib.Path(tmp_dirname)
        message_path = tmp_dir.joinpath("message")
        signatured_path = tmp_dir.joinpath(f"{message_path.name}.sig")

        with open(message_path, "w", encoding="ASCII") as fp:
            fp.write(message)

        with open(signatured_path, "wb") as fp:
            fp.write(signature)

        for ssh_key in ssh_keys:
            pkcs8_data = ssh_key.get("pkcs8_key")
            if not pkcs8_data:  # User record has no PKCS8 version
                continue

            pkcs8_key = tmp_dir.joinpath("pubkey")
            with open(pkcs8_key, "w") as fp:
                fp.write(pkcs8_data)

            pkey_util = subprocess.run(
                [
                    OPENSSL_BIN,
                    "pkeyutl",
                    "-verify",
                    "-pubin",
                    "-inkey",
                    str(pkcs8_key),
                    "-in",
                    str(message_path),
                    "-sigfile",
                    signatured_path,
                ],
                capture_output=True,
                text=True,
            )
            if pkey_util.returncode == 0:  # signature verified
                authenticated = True
                break
    if not authenticated:
        raise errors.Unauthorized("Could not find matching key for signature")

    # we're now authenticated ; generate tokens
    access_token = AccessToken.encode(user)
    refresh_token = uuid4()

    # store refresh token in database
    RefreshTokens().insert_one({
        "token":
        refresh_token,
        "user_id":
        user["_id"],
        "expire_time":
        getnow() + datetime.timedelta(days=REFRESH_TOKEN_EXPIRY),
    })

    # send response
    response_json = {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": datetime.timedelta(hours=TOKEN_EXPIRY).total_seconds(),
        "refresh_token": refresh_token,
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response