SovereignCloudStack / standards

SCS standards in a machine readable format
https://scs.community/
Creative Commons Attribution Share Alike 4.0 International
34 stars 23 forks source link

Achieve Horizon and Terraform compatibility for the Domain Manager standard #383

Closed markus-hentsch closed 9 months ago

markus-hentsch commented 12 months ago

As a CSP, I want to configure Horizon and Keystone so that a Domain Manager can use the Horizon dashboard to log in and manage user, projects and groups within a domain.

Furthermore, I want to use Terraform for such operations as well.

Horizon dashboard and Terraform usage was not yet considered in the Domain Manager standard when the draft was merged. Currently, even if domain capability is enabled in Horizon, login as a Domain Manager fails and normal user interaction with the dashboard is impaired. Terraform throws errors on specific API calls.

markus-hentsch commented 12 months ago

First, domain selection capabilities need to be enabled in Horizon's config:

OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True
OPENSTACK_KEYSTONE_DOMAIN_DROPDOWN = False

Note: OPENSTACK_KEYSTONE_DOMAIN_DROPDOWN should not be enabled. This would expose domain names^1.

Even with the feature enabled, a Domain Manager cannot log in yet with the Keystone policy settings from the current Domain Manager standard in place. It produces an error and the following Keystone log output:

DEBUG keystone.server.flask.request_processing.middleware.auth_context [None req-99231a7f-11ad-41da-aa20-5067ce761b44 None scs-test-domain-a-manager] RBAC: auth_context: {'token': <TokenModel (audit_id=jPB5r_MNThKhuJr1_Y0xSA, audit_chain_id=['jPB5r_MNThKhuJr1_Y0xSA']) at 0x7f2dceab9090>, 'domain_id': None, 'trust_id': None, 'trustor_id': None, 'trustee_id': None, 'domain_name': None, 'group_ids': [], 'user_id': '6a45c8f7b2bc4e20b4e085a516838899', 'user_domain_id': '501e4b2358254d4d9333af42b8575fb4', 'system_scope': None, 'project_id': None, 'project_domain_id': None, 'roles': [], 'is_admin_project': True, 'service_user_id': None, 'service_user_domain_id': None, 'service_project_id': None, 'service_project_domain_id': None, 'service_roles': []} {{(pid=2183629) fill_context /opt/stack/keystone/keystone/server/flask/request_processing/middleware/auth_context.py:480}}
DEBUG keystone.common.rbac_enforcer.enforcer [None req-99231a7f-11ad-41da-aa20-5067ce761b44 None scs-test-domain-a-manager] RBAC: Authorizing `identity:list_user_projects(user_id=6a45c8f7b2bc4e20b4e085a516838899)` {{(pid=2183629) enforce_call /opt/stack/keystone/keystone/common/rbac_enforcer/enforcer.py:450}}
DEBUG keystone.common.rbac_enforcer.enforcer [None req-99231a7f-11ad-41da-aa20-5067ce761b44 None scs-test-domain-a-manager] {'user_id': '6a45c8f7b2bc4e20b4e085a516838899', 'target.user.id': '6a45c8f7b2bc4e20b4e085a516838899', 'target.user.name': 'scs-test-domain-a-manager', 'target.user.domain_id': '501e4b2358254d4d9333af42b8575fb4', 'target.user.enabled': True, 'target.user.password_expires_at': None} {{(pid=2183629) _enforce /opt/stack/keystone/keystone/common/rbac_enforc
er/enforcer.py:125}}
ERROR keystone.server.flask.application [None req-99231a7f-11ad-41da-aa20-5067ce761b44 None scs-test-domain-a-manager] You are not authorized to perform the requested action: identity:list_user_projects.: keystone.exception.ForbiddenAction: You are not authorized to perform the requested action: identity:list_user_projects.
ERROR keystone.server.flask.application Traceback (most recent call last):
...
ERROR keystone.server.flask.application   File "/opt/stack/data/venv/lib/python3.10/site-packages/oslo_policy/policy.py", line 1089, in enforce
ERROR keystone.server.flask.application     raise exc(*args, **kwargs)
ERROR keystone.server.flask.application keystone.exception.ForbiddenAction: You are not authorized to perform the requested action: identity:list_user_projects.

The policy setting for identity:list_user_projects is at fault here.

markus-hentsch commented 12 months ago

The policy setting for identity:list_user_projects is at fault here.

If you look at the Keystone sample policy^2, you can find the following:

"owner": "user_id:%(user_id)s"
"admin_or_owner": "rule:admin_required or rule:owner"
"identity:list_user_projects": "rule:admin_or_owner"

Our current Domain Manager standard draft does not account^3 for the _or_owner part:

"identity:list_user_projects": "(rule:is_domain_manager and token.domain.id:%(target.user.domain_id)s) or rule:admin_required"

Once the rule is changed to:

"identity:list_user_projects": "(rule:is_domain_manager and token.domain.id:%(target.user.domain_id)s) or user_id:%(user_id)s or rule:admin_required"

... a Domain Manager can successfully log in. However, the next problem becomes apparent: the Domain Manager cannot manage (CRUD) projects etc. - they can only view IDM resources.

markus-hentsch commented 12 months ago

the Domain Manager cannot manage (CRUD) projects etc.

Horizon seems to only enable the buttons for admins.

Trying OPENSTACK_KEYSTONE_ADMIN_ROLES

I tried:

OPENSTACK_KEYSTONE_ADMIN_ROLES = ["admin", "domain-manager"]

... in Horizon's config. That did unlock some additional "Admin" sections in the sidebar which throw errors (since the Domain Manager is not a genuine admin in the backend after all) but oddly enough it did not unlock the CRUD buttons for projects etc.

Horizon code analysis

It seems that the choice whether the CRUD buttons are displayed is not related to a simple Horizon config setting. Let's look at the code for the project creation button:


class CreateProject(tables.LinkAction):
    name = "create"
    verbose_name = _("Create Project")
    url = "horizon:identity:projects:create"
    classes = ("ajax-modal",)
    icon = "plus"
    policy_rules = (('identity', 'identity:create_project'),)

    def allowed(self, request, project):
        if settings.OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT:
            # domain admin or cloud admin = True
            # project admin or member = False
            return api.keystone.is_domain_admin(request)
        return api.keystone.keystone_can_edit_project()

(https://github.com/openstack/horizon/blob/a66fe3c9dcbcd18bea7fc78365561540d2baf351/openstack_dashboard/dashboards/identity/projects/tables.py#L107-L120)

In case of multiple domains, the visibility of the button seems to depend on api.keystone.is_domain_admin() which is implemented as:

def is_domain_admin(request):
    return policy.check(
        (("identity", "admin_and_matching_domain_id"),), request)

(https://github.com/openstack/horizon/blob/master/openstack_dashboard/api/keystone.py#L325-L327)

Since the actual Keystone policy file is located in the Keystone service, Horizon cannot access it. The policy check it does here must have some other policy file serving as the basis. Maybe the behavior can be changed if whatever is the underlying file this check is based on is adjusted.

markus-hentsch commented 11 months ago

Adding to the previous findings, if logged in as a Domain Manager in Horizon, no other users or projects are visible despite them being displayed with openstack user list and openstack project list on the CLI respectively.

It seems that Horizon uses completely different requests (such as list_user_projects or get_user) when logged in as non-admin, because it does not expect normal users to succeed in retrieving the user or project list at all.

markus-hentsch commented 11 months ago

It seems that Horizon uses completely different requests (such as list_user_projects or get_user) when logged in as non-admin, because it does not expect normal users to succeed in retrieving the user or project list at all.

And for this it also seems to make checks against some policies:

class IndexView(tables.DataTableView):
    table_class = project_tables.UsersTable
    template_name = 'identity/users/index.html'
    page_title = _("Users")
    ...
    def get_data(self):
        ...
        if policy.check((("identity", "identity:list_users"),),
                        self.request):

            ...
            try:
                users = api.keystone.user_list(self.request,
                                               domain=domain_id,
                                               filters=filters)
            ...
        elif policy.check((("identity", "identity:get_user"),),
                          self.request):
            try:
                user = api.keystone.user_get(self.request,
                                             self.request.user.id,
                                             admin=False)
            ...

(https://github.com/openstack/horizon/blob/79c1d158e0bc9a2d8951ca1e8944e95b3dc7a303/openstack_dashboard/dashboards/identity/users/views.py#L65-L89)

I need to get to the bottom of these obscure policy checks that Horizon does. Horizon cannot physically access Keystone's policy files so it must have some kind of copy or generated defaults? If we could change those maybe we can unlock a lot of functionality for the Domain Manager.

markus-hentsch commented 11 months ago

FTR, we discussed^1 that we do not want to jump the gun and implement full Horizon support for Domain Managers in a way that enables the Domain Manager workflow in the dashboard. Reasoning was that for Domain Managers limited API access (e.g. Keystone only) might be provided and full dashboard usage might not be intended to begin with. Furthermore, debugging and development effort would be required to make Horizon compatible (see comments above).

Due to this, the work on this issue and the linked PR will address:

The PR will not address:

josephineSei commented 10 months ago

As I will take over the work from @markus-hentsch I am reading through the whole Domain Manager work until now