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

core: app entitlements #12090

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

core: app entitlements #12090

wants to merge 6 commits into from

Conversation

BeryJu
Copy link
Member

@BeryJu BeryJu commented Nov 20, 2024

Details

Application scoped entitlements which can be used to configure authorization within a given application. Entitlements can be assigned to individual users or groups and can store attributes

  • Add scope mapping for entitlements
  • Add docs
  • Add note that policies are not supported for bindings to the entitlements yet
  • Add validation that policies cant be set in bindings to this target type

Checklist

  • Local tests pass (ak test authentik/)
  • The code has been formatted (make lint-fix)

If an API change has been made

  • The API schema has been updated (make gen-build)

If changes to the frontend have been made

  • The code has been formatted (make web)

If applicable

  • The documentation has been updated
  • The documentation has been formatted (make website)

Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
@BeryJu BeryJu requested review from a team as code owners November 20, 2024 14:12
Copy link

netlify bot commented Nov 20, 2024

Deploy Preview for authentik-docs canceled.

Name Link
🔨 Latest commit f40144d
🔍 Latest deploy log https://app.netlify.com/sites/authentik-docs/deploys/673e0ecf37a4600008fa0c67

Copy link

netlify bot commented Nov 20, 2024

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit f40144d
🔍 Latest deploy log https://app.netlify.com/sites/authentik-storybook/deploys/673e0ecf3ba7800008214543
😎 Deploy Preview https://deploy-preview-12090--authentik-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

codecov bot commented Nov 20, 2024

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1605 2 1603 1
View the top 2 failed tests by shortest run time
authentik.providers.oauth2.tests.test_userinfo.TestUserinfo::test_userinfo_invalid_scope
Stack Traces | 1.44s run time
self = <unittest.case._Outcome object at 0x7f3c53456900>
test_case = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
result = <TestCaseFunction test_userinfo_invalid_scope>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:634: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
method = <bound method TestUserinfo.test_userinfo_invalid_scope of <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:589: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>

    def test_userinfo_invalid_scope(self):
        """test user info with a broken scope"""
        scope = ScopeMapping.objects.create(name="test", scope_name="openid", expression="q")
        self.provider.property_mappings.add(scope)
    
        res = self.client.get(
            reverse("authentik_providers_oauth2:userinfo"),
            HTTP_AUTHORIZATION=f"Bearer {self.token.token}",
        )
>       self.assertJSONEqual(
            res.content.decode(),
            {
                "name": self.user.name,
                "given_name": self.user.name,
                "preferred_username": self.user.name,
                "nickname": self.user.name,
                "groups": [group.name for group in self.user.ak_groups.all()],
                "sub": "bar",
            },
        )

.../oauth2/tests/test_userinfo.py:77: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
raw = '{"sub": "bar", "name": "NiCeWfQ9xGKsJzXAXpBj", "given_name": "NiCeWfQ9xGKsJzXAXpBj", "preferred_username": "NiCeWfQ9xGKsJzXAXpBj", "nickname": "NiCeWfQ9xGKsJzXAXpBj", "groups": ["NiCeWfQ9xGKsJzXAXpBj"], "entitlements": [], "roles": []}'
expected_data = {'given_name': 'NiCeWfQ9xGKsJzXAXpBj', 'groups': ['NiCeWfQ9xGKsJzXAXpBj'], 'name': 'NiCeWfQ9xGKsJzXAXpBj', 'nickname': 'NiCeWfQ9xGKsJzXAXpBj', ...}
msg = None

    def assertJSONEqual(self, raw, expected_data, msg=None):
        """
        Assert that the JSON fragments raw and expected_data are equal.
        Usual JSON non-significant whitespace rules apply as the heavyweight
        is delegated to the json library.
        """
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            self.fail("First argument is not valid JSON: %r" % raw)
        if isinstance(expected_data, str):
            try:
                expected_data = json.loads(expected_data)
            except ValueError:
                self.fail("Second argument is not valid JSON: %r" % expected_data)
>       self.assertEqual(data, expected_data, msg=msg)

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/test/testcases.py:922: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
first = {'entitlements': [], 'given_name': 'NiCeWfQ9xGKsJzXAXpBj', 'groups': ['NiCeWfQ9xGKsJzXAXpBj'], 'name': 'NiCeWfQ9xGKsJzXAXpBj', ...}
second = {'given_name': 'NiCeWfQ9xGKsJzXAXpBj', 'groups': ['NiCeWfQ9xGKsJzXAXpBj'], 'name': 'NiCeWfQ9xGKsJzXAXpBj', 'nickname': 'NiCeWfQ9xGKsJzXAXpBj', ...}
msg = None

    def assertEqual(self, first, second, msg=None):
        """Fail if the two objects are unequal as determined by the '=='
           operator.
        """
        assertion_func = self._getAssertEqualityFunc(first, second)
>       assertion_func(first, second, msg=msg)

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:885: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
d1 = {'entitlements': [], 'given_name': 'NiCeWfQ9xGKsJzXAXpBj', 'groups': ['NiCeWfQ9xGKsJzXAXpBj'], 'name': 'NiCeWfQ9xGKsJzXAXpBj', ...}
d2 = {'given_name': 'NiCeWfQ9xGKsJzXAXpBj', 'groups': ['NiCeWfQ9xGKsJzXAXpBj'], 'name': 'NiCeWfQ9xGKsJzXAXpBj', 'nickname': 'NiCeWfQ9xGKsJzXAXpBj', ...}
msg = None

    def assertDictEqual(self, d1, d2, msg=None):
        self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
        self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
    
        if d1 != d2:
            standardMsg = '%s != %s' % _common_shorten_repr(d1, d2)
            diff = ('\n' + '\n'.join(difflib.ndiff(
                           pprint.pformat(d1).splitlines(),
                           pprint.pformat(d2).splitlines())))
            standardMsg = self._truncateMessage(standardMsg, diff)
>           self.fail(self._formatMessage(msg, standardMsg))

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:1184: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_invalid_scope>
msg = "{'sub': 'bar', 'name': 'NiCeWfQ9xGKsJzXAXpB[187 chars]: []} != {'name': 'NiCeWfQ9xGKsJzXAXpBj', 'given_nam[154 chars]...ickname': 'NiCeWfQ9xGKsJzXAXpBj',\n   'preferred_username': 'NiCeWfQ9xGKsJzXAXpBj',\n-  'roles': [],\n   'sub': 'bar'}"

    def fail(self, msg=None):
        """Fail immediately, with the given message."""
>       raise self.failureException(msg)
E       AssertionError: {'sub': 'bar', 'name': 'NiCeWfQ9xGKsJzXAXpB[187 chars]: []} != {'name': 'NiCeWfQ9xGKsJzXAXpBj', 'given_nam[154 chars]bar'}
E       - {'entitlements': [],
E       -  'given_name': 'NiCeWfQ9xGKsJzXAXpBj',
E       ? ^
E       
E       + {'given_name': 'NiCeWfQ9xGKsJzXAXpBj',
E       ? ^
E       
E          'groups': ['NiCeWfQ9xGKsJzXAXpBj'],
E          'name': 'NiCeWfQ9xGKsJzXAXpBj',
E          'nickname': 'NiCeWfQ9xGKsJzXAXpBj',
E          'preferred_username': 'NiCeWfQ9xGKsJzXAXpBj',
E       -  'roles': [],
E          'sub': 'bar'}

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:715: AssertionError
authentik.providers.oauth2.tests.test_userinfo.TestUserinfo::test_userinfo_normal
Stack Traces | 1.46s run time
self = <unittest.case._Outcome object at 0x7f3c5117b9b0>
test_case = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
result = <TestCaseFunction test_userinfo_normal>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:634: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
method = <bound method TestUserinfo.test_userinfo_normal of <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:589: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>

    def test_userinfo_normal(self):
        """test user info with all normal scopes"""
        res = self.client.get(
            reverse("authentik_providers_oauth2:userinfo"),
            HTTP_AUTHORIZATION=f"Bearer {self.token.token}",
        )
>       self.assertJSONEqual(
            res.content.decode(),
            {
                "name": self.user.name,
                "given_name": self.user.name,
                "preferred_username": self.user.name,
                "nickname": self.user.name,
                "groups": [group.name for group in self.user.ak_groups.all()],
                "sub": "bar",
            },
        )

.../oauth2/tests/test_userinfo.py:55: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
raw = '{"sub": "bar", "name": "9Sx7UTrX8nbv3bCl4rIO", "given_name": "9Sx7UTrX8nbv3bCl4rIO", "preferred_username": "9Sx7UTrX8nbv3bCl4rIO", "nickname": "9Sx7UTrX8nbv3bCl4rIO", "groups": ["9Sx7UTrX8nbv3bCl4rIO"], "entitlements": [], "roles": []}'
expected_data = {'given_name': '9Sx7UTrX8nbv3bCl4rIO', 'groups': ['9Sx7UTrX8nbv3bCl4rIO'], 'name': '9Sx7UTrX8nbv3bCl4rIO', 'nickname': '9Sx7UTrX8nbv3bCl4rIO', ...}
msg = None

    def assertJSONEqual(self, raw, expected_data, msg=None):
        """
        Assert that the JSON fragments raw and expected_data are equal.
        Usual JSON non-significant whitespace rules apply as the heavyweight
        is delegated to the json library.
        """
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            self.fail("First argument is not valid JSON: %r" % raw)
        if isinstance(expected_data, str):
            try:
                expected_data = json.loads(expected_data)
            except ValueError:
                self.fail("Second argument is not valid JSON: %r" % expected_data)
>       self.assertEqual(data, expected_data, msg=msg)

../../../..../pypoetry/virtualenvs/authentik-xvtLQ9eE-py3.12/lib/python3.12.../django/test/testcases.py:922: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
first = {'entitlements': [], 'given_name': '9Sx7UTrX8nbv3bCl4rIO', 'groups': ['9Sx7UTrX8nbv3bCl4rIO'], 'name': '9Sx7UTrX8nbv3bCl4rIO', ...}
second = {'given_name': '9Sx7UTrX8nbv3bCl4rIO', 'groups': ['9Sx7UTrX8nbv3bCl4rIO'], 'name': '9Sx7UTrX8nbv3bCl4rIO', 'nickname': '9Sx7UTrX8nbv3bCl4rIO', ...}
msg = None

    def assertEqual(self, first, second, msg=None):
        """Fail if the two objects are unequal as determined by the '=='
           operator.
        """
        assertion_func = self._getAssertEqualityFunc(first, second)
>       assertion_func(first, second, msg=msg)

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:885: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
d1 = {'entitlements': [], 'given_name': '9Sx7UTrX8nbv3bCl4rIO', 'groups': ['9Sx7UTrX8nbv3bCl4rIO'], 'name': '9Sx7UTrX8nbv3bCl4rIO', ...}
d2 = {'given_name': '9Sx7UTrX8nbv3bCl4rIO', 'groups': ['9Sx7UTrX8nbv3bCl4rIO'], 'name': '9Sx7UTrX8nbv3bCl4rIO', 'nickname': '9Sx7UTrX8nbv3bCl4rIO', ...}
msg = None

    def assertDictEqual(self, d1, d2, msg=None):
        self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
        self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
    
        if d1 != d2:
            standardMsg = '%s != %s' % _common_shorten_repr(d1, d2)
            diff = ('\n' + '\n'.join(difflib.ndiff(
                           pprint.pformat(d1).splitlines(),
                           pprint.pformat(d2).splitlines())))
            standardMsg = self._truncateMessage(standardMsg, diff)
>           self.fail(self._formatMessage(msg, standardMsg))

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:1184: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.providers.oauth2.tests.test_userinfo.TestUserinfo testMethod=test_userinfo_normal>
msg = "{'sub': 'bar', 'name': '9Sx7UTrX8nbv3bCl4rI[187 chars]: []} != {'name': '9Sx7UTrX8nbv3bCl4rIO', 'given_nam[154 chars]...ickname': '9Sx7UTrX8nbv3bCl4rIO',\n   'preferred_username': '9Sx7UTrX8nbv3bCl4rIO',\n-  'roles': [],\n   'sub': 'bar'}"

    def fail(self, msg=None):
        """Fail immediately, with the given message."""
>       raise self.failureException(msg)
E       AssertionError: {'sub': 'bar', 'name': '9Sx7UTrX8nbv3bCl4rI[187 chars]: []} != {'name': '9Sx7UTrX8nbv3bCl4rIO', 'given_nam[154 chars]bar'}
E       - {'entitlements': [],
E       -  'given_name': '9Sx7UTrX8nbv3bCl4rIO',
E       ? ^
E       
E       + {'given_name': '9Sx7UTrX8nbv3bCl4rIO',
E       ? ^
E       
E          'groups': ['9Sx7UTrX8nbv3bCl4rIO'],
E          'name': '9Sx7UTrX8nbv3bCl4rIO',
E          'nickname': '9Sx7UTrX8nbv3bCl4rIO',
E          'preferred_username': '9Sx7UTrX8nbv3bCl4rIO',
E       -  'roles': [],
E          'sub': 'bar'}

.../hostedtoolcache/Python/3.12.7................../x64/lib/python3.12/unittest/case.py:715: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
Got feedback? Let us know on Github

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant