Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create SECURITY.md #1

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @JimmyPettersson85 @xernobyl @yaziine
* @JimmyPettersson85 @xernobyl
8 changes: 2 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,12 @@ jobs:
strategy:
max-parallel: 1
matrix:
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # gives the commit linter access to previous commits

- name: Commit message linter
if: ${{ matrix.python == '3.7' }}
uses: wagoid/commitlint-github-action@v4

- uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python }}
Expand All @@ -34,7 +30,7 @@ jobs:
run: pip install -q ".[test, ci]"

- name: Lint with ${{ matrix.python }}
if: ${{ matrix.python == '3.7' }}
if: ${{ matrix.python == '3.8' }}
run: make lint

- name: Install, test and code coverage with ${{ matrix.python }}
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### [5.3.1](https://github.com/GetStream/stream-python/compare/v5.2.1...v5.3.1) (2023-10-25)

### [5.2.1](https://github.com/GetStream/stream-python/compare/v5.2.0...v5.2.1) (2023-02-27)

## [5.2.0](https://github.com/GetStream/stream-python/compare/v5.1.1...v5.2.0) (2023-02-16)
Expand Down
16 changes: 16 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Reporting a Vulnerability
At Stream we are committed to the security of our Software. We appreciate your efforts in disclosing vulnerabilities responsibly and we will make every effort to acknowledge your contributions.

Report security vulnerabilities at the following email address:
```
[[email protected]](mailto:[email protected])
```
Alternatively it is also possible to open a new issue in the affected repository, tagging it with the `security` tag.

A team member will acknowledge the vulnerability and will follow-up with more detailed information. A representative of the security team will be in touch if more information is needed.

# Information to include in a report
While we appreciate any information that you are willing to provide, please make sure to include the following:
* Which repository is affected
* Which branch, if relevant
* Be as descriptive as possible, the team will replicate the vulnerability before working on a fix.
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from stream import __version__, __maintainer__, __email__, __license__

install_requires = [
"requests>=2.28.0,<3",
"pyjwt>=2.6.0,<3",
"pytz>=2022.7.1",
"aiohttp>=3.8.4",
"requests>=2.31.0,<3",
"pyjwt>=2.8.0,<3",
"pytz>=2023.3.post1",
"aiohttp>=3.9.0b0",
]
tests_require = ["pytest", "pytest-cov", "python-dateutil", "pytest-asyncio"]
ci_require = ["black", "flake8", "pytest-cov"]
Expand Down
5 changes: 4 additions & 1 deletion stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
__copyright__ = "Copyright 2022, Stream.io, Inc"
__credits__ = ["Thierry Schellenbach, mellowmorning.com, @tschellenbach"]
__license__ = "BSD-3-Clause"
__version__ = "5.2.1"
__version__ = "5.3.1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

CHANGELOG.md is not updated to reflect the new version.

  • stream/__init__.py is updated to version 5.3.1.
  • CHANGELOG.md still lists version 5.2.0.

Please update CHANGELOG.md to include the new version 5.3.1.

🔗 Analysis chain

Version update looks good. Verify consistency across the project.

The version bump from 5.2.1 to 5.3.1 is appropriate given the new features mentioned in the summary.

Please ensure this version update is consistent across the project. Run the following script to check:


Version inconsistency detected.

  • The __init__.py file is updated to version 5.3.1, but the CHANGELOG.md reflects version 5.2.0.
  • Unable to verify the version in setup.py due to a file type recognition issue.

Please run the following script to check the version in setup.py without specifying the file type:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check version consistency across key files

# Test: Check version in setup.py
echo "Version in setup.py:"
rg --type python 'version\s*=' setup.py

# Test: Check for version update in CHANGELOG.md
echo "Latest version in CHANGELOG.md:"
rg '^## \[' CHANGELOG.md | head -n 1

# Test: Check version in __init__.py (for confirmation)
echo "Version in __init__.py:"
rg '__version__\s*=' stream/__init__.py

Length of output: 448


Script:

# 
#!/bin/bash
# Description: Check version consistency in setup.py without type specification

echo "Version in setup.py:"
rg 'version\s*=' setup.py

Length of output: 103

__maintainer__ = "Thierry Schellenbach"
__email__ = "[email protected]"
__status__ = "Production"
Expand All @@ -31,6 +31,9 @@ def connect(
"""
from stream.client import AsyncStreamClient, StreamClient

if location is None:
location = os.environ.get("STREAM_REGION")

stream_url = os.environ.get("STREAM_URL")
# support for the heroku STREAM_URL syntax
if stream_url and not api_key:
Expand Down
6 changes: 5 additions & 1 deletion stream/reactions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ def update(self, reaction_id, data=None, target_feeds=None):
pass

@abstractmethod
def delete(self, reaction_id):
def delete(self, reaction_id, soft=False):
pass

@abstractmethod
def restore(self, reaction_id):
pass
Comment on lines +26 to 31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Add tests for restore and soft delete functionalities.

Currently, there are no tests covering the restore method and the soft parameter in the delete method. To ensure these functionalities work as intended, please add the following tests:

  1. restore Method:

    • Verify that the method correctly restores a previously deleted reaction.
    • Ensure appropriate handling of invalid reaction_id inputs.
  2. soft Parameter in delete Method:

    • Test both scenarios where soft is True and False.
    • Confirm that soft deletes mark reactions appropriately without permanent removal.
    • Ensure that the behavior aligns with the application's requirements.

Implementing these tests will enhance the reliability and maintainability of the AbstractReactions implementations.

🔗 Analysis chain

Ensure concrete implementations and tests are updated.

The changes to the AbstractReactions class will require updates to concrete implementations and tests:

  1. The BaseReactions class in this file, and any other concrete implementations of AbstractReactions in the codebase, will need to:

    • Implement the new restore method.
    • Update the delete method to handle the soft parameter.
  2. Existing tests should be updated, and new tests should be added to cover:

    • The soft delete functionality.
    • The restore functionality.

To help identify the classes that need to be updated, you can run the following script:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find all Python files that likely contain implementations of AbstractReactions
echo "Files potentially containing AbstractReactions implementations:"
rg --type python -l "class \w+\(.*AbstractReactions.*\):"

# Find all test files related to reactions
echo -e "\nTest files related to reactions:"
fd -e py test.*reaction

Length of output: 336


Script:

#!/bin/bash
# Corrected script to find all Python files that likely contain implementations of AbstractReactions
echo "Files potentially containing AbstractReactions implementations:"
rg --type py -l "class \w+\(.*AbstractReactions.*\):"

# Corrected script to find all test files related to reactions with a more flexible pattern
echo -e "\nTest files related to reactions:"
fd -e py -g "*test*reaction*"

Length of output: 330


Script:

#!/bin/bash
# Find all Python test files that mention 'reaction' in their filenames
echo "All Python test files related to reactions:"
rg --type py -l "reaction" <<< "$(fd -e py)"

# Additionally, search within test files for mentions of 'restore' or 'soft delete'
echo -e "\nSearching within test files for 'restore' or 'soft delete' functionalities:"
rg --type py "(def restore|soft\s*delete)" -- test/

Length of output: 422


Script:

#!/bin/bash
# Find all Python test files that include 'test' in their filenames, regardless of their location
echo "All Python test files related to reactions:"
fd -e py -g "*test*.py"

# Capture the list of test files
TEST_FILES=$(fd -e py -g "*test*.py")

# Check if any test files were found
if [ -z "$TEST_FILES" ]; then
    echo -e "\nNo test files related to reactions were found."
else
    echo -e "\nSearching within test files for 'restore' or 'soft delete' functionalities:"
    # Search within the found test files for 'restore' or 'soft delete'
    rg --type py "(def restore|soft\s*delete)" $TEST_FILES
fi

Length of output: 751


@abstractmethod
Expand Down
22 changes: 20 additions & 2 deletions stream/reactions/reaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ def update(self, reaction_id, data=None, target_feeds=None):
data=payload,
)

def delete(self, reaction_id):
def delete(self, reaction_id, soft=False):
url = f"{self.API_ENDPOINT}{reaction_id}"
return self.client.delete(
url,
service_name=self.SERVICE_NAME,
signature=self.token,
params={"soft": soft},
)

def restore(self, reaction_id):
url = f"{self.API_ENDPOINT}{reaction_id}/restore"
return self.client.put(
url, service_name=self.SERVICE_NAME, signature=self.token
)

Expand Down Expand Up @@ -123,9 +132,18 @@ async def update(self, reaction_id, data=None, target_feeds=None):
data=payload,
)

async def delete(self, reaction_id):
async def delete(self, reaction_id, soft=False):
url = f"{self.API_ENDPOINT}{reaction_id}"
return await self.client.delete(
url,
service_name=self.SERVICE_NAME,
signature=self.token,
params={"soft": soft},
)

async def restore(self, reaction_id):
url = f"{self.API_ENDPOINT}{reaction_id}/restore"
return await self.client.put(
url, service_name=self.SERVICE_NAME, signature=self.token
)

Expand Down
40 changes: 39 additions & 1 deletion stream/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dateutil.tz import tzlocal

import stream
from stream.exceptions import ApiKeyException, InputException
from stream.exceptions import ApiKeyException, InputException, DoesNotExistException


def assert_first_activity_id_equal(activities, correct_activity_id):
Expand Down Expand Up @@ -1049,6 +1049,44 @@ async def test_reaction_delete(async_client):
await async_client.reactions.delete(response["id"])


@pytest.mark.asyncio
async def test_reaction_hard_delete(async_client):
response = await async_client.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
await async_client.reactions.delete(response["id"], soft=False)


@pytest.mark.asyncio
async def test_reaction_soft_delete(async_client):
response = await async_client.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
await async_client.reactions.delete(response["id"], soft=True)


@pytest.mark.asyncio
async def test_reaction_soft_delete_and_restore(async_client):
response = await async_client.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
await async_client.reactions.delete(response["id"], soft=True)
r1 = await async_client.reactions.get(response["id"])
assert r1.get("deleted_at", None) is not None
await async_client.reactions.restore(response["id"])
r1 = await async_client.reactions.get(response["id"])
assert "deleted_at" not in r1


@pytest.mark.asyncio
async def test_reaction_invalid_restore(async_client):
response = await async_client.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
with pytest.raises(DoesNotExistException):
await async_client.reactions.restore(response["id"])


@pytest.mark.asyncio
async def test_reaction_add_child(async_client):
response = await async_client.reactions.add(
Expand Down
39 changes: 35 additions & 4 deletions stream/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import stream
from stream import serializer
from stream.exceptions import ApiKeyException, InputException
from stream.exceptions import ApiKeyException, InputException, DoesNotExistException
from stream.feed import Feed


Expand Down Expand Up @@ -150,14 +150,14 @@ def test_api_url(self):
)

def test_collections_url_default(self):
c = stream.connect("key", "secret")
c = stream.connect("key", "secret", location="")
feed_url = c.get_full_url(relative_url="meta/", service_name="api")

if not self.local_tests:
self.assertEqual(feed_url, "https://api.stream-io-api.com/api/v1.0/meta/")

def test_personalization_url_default(self):
c = stream.connect("key", "secret")
c = stream.connect("key", "secret", location="")
feed_url = c.get_full_url(
relative_url="recommended", service_name="personalization"
)
Expand All @@ -169,7 +169,7 @@ def test_personalization_url_default(self):
)

def test_api_url_default(self):
c = stream.connect("key", "secret")
c = stream.connect("key", "secret", location="")
feed_url = c.get_full_url(service_name="api", relative_url="feed/")

if not self.local_tests:
Expand Down Expand Up @@ -1439,6 +1439,37 @@ def test_reaction_delete(self):
)
self.c.reactions.delete(response["id"])

def test_reaction_hard_delete(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=False)

Comment on lines +1442 to +1447
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add assertions to verify reaction hard deletion

The method test_reaction_hard_delete performs a hard delete on a reaction but does not include assertions to verify that the reaction has been successfully deleted. Consider adding assertions to confirm that the reaction no longer exists after deletion.

Apply this diff to add the assertion:

 def test_reaction_hard_delete(self):
     response = self.c.reactions.add(
         "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
     )
     self.c.reactions.delete(response["id"], soft=False)
+    self.assertRaises(
+        DoesNotExistException, lambda: self.c.reactions.get(response["id"])
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_reaction_hard_delete(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=False)
def test_reaction_hard_delete(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=False)
self.assertRaises(
DoesNotExistException, lambda: self.c.reactions.get(response["id"])
)

def test_reaction_soft_delete(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=True)

Comment on lines +1448 to +1453
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add assertions to verify reaction soft deletion

In test_reaction_soft_delete, after performing a soft delete, there are no assertions to verify that the reaction is marked as deleted. Adding assertions to check the deleted_at attribute can ensure that the soft delete functionality is working as expected.

Apply this diff to add the assertion:

 def test_reaction_soft_delete(self):
     response = self.c.reactions.add(
         "like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
     )
     self.c.reactions.delete(response["id"], soft=True)
+    r1 = self.c.reactions.get(response["id"])
+    self.assertIsNotNone(r1.get("deleted_at"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_reaction_soft_delete(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=True)
def test_reaction_soft_delete(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=True)
r1 = self.c.reactions.get(response["id"])
self.assertIsNotNone(r1.get("deleted_at"))

def test_reaction_soft_delete_and_restore(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.c.reactions.delete(response["id"], soft=True)
r1 = self.c.reactions.get(response["id"])
self.assertIsNot(r1["deleted_at"], None)
self.c.reactions.restore(response["id"])
r1 = self.c.reactions.get(response["id"])
self.assertTrue("deleted_at" not in r1)

def test_reaction_invalid_restore(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
)
self.assertRaises(
DoesNotExistException, lambda: self.c.reactions.restore(response["id"])
)

Comment on lines +1465 to +1472
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Verify correct exception is raised when restoring non-deleted reaction

In test_reaction_invalid_restore, the test attempts to restore a reaction that has not been deleted and expects a DoesNotExistException. Please confirm that attempting to restore a non-deleted reaction raises the DoesNotExistException. If this is not the expected behavior, consider updating the test or handling a different exception.

def test_reaction_add_child(self):
response = self.c.reactions.add(
"like", "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", "mike"
Expand Down