Exemplo n.º 1
0
    def refresh_token_grant(old_refresh_token: UUID):
        """Implements logic for refresh token grant."""

        # check token exists in database and get expire time and user id
        collection = RefreshTokens()
        old_token_document = collection.find_one(
            {"token": old_refresh_token}, {"expire_time": 1, "user_id": 1}
        )
        if old_token_document is None:
            raise InvalidGrant("Refresh token is invalid.")

        # check token is not expired
        expire_time = old_token_document["expire_time"]
        if expire_time < datetime.now():
            raise InvalidGrant("Refresh token is expired.")

        # check user exists
        user_id = old_token_document["user_id"]
        user = Users().find_one({"_id": user_id}, {"password_hash": 0})
        if user is None:
            raise InvalidGrant("Refresh token is invalid.")

        # generate token
        access_token = LoadedAccessToken(
            user["_id"], user["username"], user.get("scope", {})
        ).encode()
        refresh_token = OAuth2.generate_refresh_token(user["_id"])

        # delete old refresh token from database
        collection.delete_one({"token": old_refresh_token})
        collection.delete_many({"expire_time": {"$lte": datetime.now()}})

        return OAuth2.success_response(access_token, refresh_token)
Exemplo n.º 2
0
    def get(self, token: AccessToken.Payload):

        request_args = SkipLimitSchema().load(request.args.to_dict())
        skip, limit = request_args["skip"], request_args["limit"]

        # get users from database
        query = {}
        count = Users().count_documents(query)
        cursor = (
            Users()
            .find(query, {"_id": 0, "username": 1, "email": 1, "scope": 1})
            .skip(skip)
            .limit(limit)
        )

        # add role to user while removing scope
        def _add_role(user):
            user.update({"role": get_role_for(user.pop("scope", {}))})
            return user

        users = list(map(_add_role, cursor))

        return jsonify(
            {"meta": {"skip": skip, "limit": limit, "count": count}, "items": users}
        )
Exemplo n.º 3
0
    def get(self, username: str, token: AccessToken.Payload):
        # if user in url is not user in token, check user permission
        if username != token.username:
            if not token.get_permission("users", "ssh_keys"):
                raise errors.NotEnoughPrivilege()

        user = Users().find_one({"username": username}, {"ssh_keys": 1})
        if user is None:
            raise errors.NotFound()

        ssh_keys = user.get("ssh_keys", [])
        return jsonify(ssh_keys)
Exemplo n.º 4
0
def credentials():
    """
    Authorize a user with username and password
    When success, return json object with access and refresh token
    """

    # get username and password from request header
    if "application/x-www-form-urlencoded" in request.content_type:
        username = request.form.get("username")
        password = request.form.get("password")
    else:
        username = request.headers.get("username")
        password = request.headers.get("password")
    if username is None or password is None:
        raise BadRequest("missing username or password")

    # check user exists
    user = Users().find_one({"username": username}, {
        "username": 1,
        "scope": 1,
        "password_hash": 1
    })
    if user is None:
        raise Unauthorized("this user does not exist")

    # check password is valid
    password_hash = user.pop("password_hash")
    is_valid = check_password_hash(password_hash, password)
    if not is_valid:
        raise Unauthorized("password does not match")

    # generate token
    access_token = AccessToken.encode(user)
    access_expires = AccessToken.get_expiry(access_token)
    refresh_token = create_refresh_token(user["username"])

    # send response
    response_json = {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": access_expires,
        "refresh_token": refresh_token,
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response
Exemplo n.º 5
0
    def get(self, token: AccessToken.Payload, username: str):
        # if user in url is not user in token, check user permission
        if username != token.username:
            if not token.get_permission("users", "read"):
                raise errors.NotEnoughPrivilege()

        # find user based on _id or username
        user = Users().find_one(
            {"username": username},
            {"_id": 0, "username": 1, "email": 1, "scope": 1, "ssh_keys": 1},
        )

        if user is None:
            raise errors.NotFound()

        user["role"] = get_role_for(user.get("scope"))

        return jsonify(user)
Exemplo n.º 6
0
 def delete(self, token: AccessToken.Payload, username: str):
     # delete user
     deleted_count = Users().delete_one({
         "username": username
     }).deleted_count
     if deleted_count == 0:
         raise errors.NotFound()
     Workers().delete_many({"username": username})
     return Response(status=HTTPStatus.NO_CONTENT)
Exemplo n.º 7
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.º 8
0
    def get(self, username: str, fingerprint: str):
        # list of permission to test the matching user against
        requested_permissions = request.args.getlist("with_permission") or []

        query = {}
        # request using `-` as username searchs on all users
        if username != "-":
            query.update({"username": username})

        query.update({"ssh_keys.fingerprint": fingerprint})

        # database
        user = Users().find_one(
            query,
            {
                "username": 1,
                "scope": 1,
                "ssh_keys": 1,
            },
        )

        # no user means no matching SSH key for fingerprint
        if not user:
            raise errors.NotFound()

        key = [
            key for key in user["ssh_keys"]
            if key["fingerprint"] == fingerprint
        ][-1]

        for permission in requested_permissions:
            namespace, perm_name = permission.split(".", 1)
            if not user.get("scope", {}).get(namespace, {}).get(perm_name):
                raise errors.NotEnoughPrivilege(permission)

        payload = {
            "username": user["username"],
            "key": key["key"],
            "type": key["type"],
            "name": key["name"],
        }

        return jsonify(payload)
Exemplo n.º 9
0
    def password_grant(username: str, password: str):
        """Implements logic for password grant."""

        # check user exists
        user = Users().find_one({"username": username})
        if user is None:
            raise InvalidGrant("Username or password is invalid.")

        # check password is valid
        password_hash = user.pop("password_hash")
        is_valid = check_password_hash(password_hash, password)
        if not is_valid:
            raise InvalidGrant("Username or password is invalid.")

        # generate token
        access_token = LoadedAccessToken(
            user["_id"], user["username"], user.get("scope", {})
        ).encode()
        refresh_token = OAuth2.generate_refresh_token(user["_id"])

        return OAuth2.success_response(access_token, refresh_token)
Exemplo n.º 10
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.º 11
0
def refresh_token():
    """
    Issue a new set of access and refresh token after validating an old refresh token
    Old refresh token can only be used once and hence is removed from database
    Unused but expired refresh token is also deleted from database
    """

    # get old refresh token from request header
    old_token = request.headers.get("refresh-token")
    if old_token is None:
        raise BadRequest("missing refresh-token")

    # check token exists in database and get expire time and user id
    try:
        old_token_document = RefreshTokens().find_one(
            {"token": UUIDLegacy(UUID(old_token))}, {
                "expire_time": 1,
                "username": 1
            })
        if old_token_document is None:
            raise Unauthorized("refresh-token invalid")
    except Exception:
        raise Unauthorized("refresh-token invalid")

    # check token is not expired
    if old_token_document["expire_time"] < getnow():
        raise Unauthorized("token expired")

    # check user exists
    user = Users().find_one({"username": old_token_document["username"]}, {
        "username": 1,
        "scope": 1
    })
    if user is None:
        raise Unauthorized("user not found")

    # generate token
    access_token = AccessToken.encode(user)
    refresh_token = create_refresh_token(user["username"])
    # delete old refresh token from database
    RefreshTokens().delete_one({"token": UUID(old_token)})

    # send response
    response_json = {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": AccessToken.get_expiry(access_token),
        "refresh_token": refresh_token,
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response
Exemplo n.º 12
0
    def delete(self, username: str, fingerprint: str, token: AccessToken.Payload):
        # if user in url is not user in token, check user permission
        if username != token.username:
            if not token.get_permission("users", "ssh_keys"):
                raise errors.NotEnoughPrivilege()

        # database
        result = Users().update_one(
            {"username": username},
            {"$pull": {"ssh_keys": {"fingerprint": fingerprint}}},
        )

        if result.modified_count > 0:
            Response(status=HTTPStatus.NO_CONTENT)
        else:
            raise errors.NotFound()

        return Response(status=HTTPStatus.NO_CONTENT)
Exemplo n.º 13
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.º 14
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.º 15
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.º 16
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