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

WIP: FEATURE: Content Repository Privileges #4185

Closed
wants to merge 1 commit into from

Conversation

bwaidelich
Copy link
Member

Related: #3732

@github-actions github-actions bot added the 9.0 label Apr 12, 2023
$privileges = $this->privilegeProvider->getPrivileges($visibilityConstraints);
if (!$privileges->isContentStreamAllowed($contentStreamId)) {
throw new \RuntimeException(sprintf('No access to content stream "%s"', $contentStreamId->value), 1681306937);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

does it make sense to check the privileges here already?

Copy link
Member

Choose a reason for hiding this comment

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

I would say yes (if we can, like it is done here). Ofc people could work around this if they manually instanciated ContentSubgraph; but we could never prevent this.

Copy link
Member Author

@bwaidelich bwaidelich Oct 24, 2023

Choose a reason for hiding this comment

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

rename to sth like $this->privilegeProvider->isFetchingOfNodesAllowedInSubgraph($contentStreamId, $dimensionSpacePoint) (consider using result object for more context)

/**
* @api
*/
final class Privileges
Copy link
Member Author

Choose a reason for hiding this comment

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

This is missing the "restriction edge attributes" still

Copy link
Member Author

Choose a reason for hiding this comment

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

(see #4557 -> but they will be called node tags as discussed)

Copy link
Member Author

Choose a reason for hiding this comment

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

Rename to sth like ReadPrivileges

if (!$privilegeProviderFactory instanceof PrivilegeProviderFactoryInterface) {
throw InvalidConfigurationException::fromMessage('privilegeProvider.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, PrivilegeProviderFactoryInterface::class, get_debug_type($privilegeProviderFactory));
}
return $privilegeProviderFactory->build($contentRepositoryId, $contentRepositorySettings['userIdProvider']['options'] ?? [], $userIdProvider, $contentRepositoryRegistry);
Copy link
Member Author

Choose a reason for hiding this comment

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

The PrivilegeProvider currently has a dependency to...

  • the UserIdProvider to load policies of the authenticated user
  • the ContentRepositoryRegistry to retrieve ContentStreams for the user (via WorkspaceFinder)..

This is nasty

Copy link
Member

Choose a reason for hiding this comment

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

UserIdProvider is fine IMHO, ContentRepositoryRegistry should go away. What about making the $contentRepository available to the provider at runtime? would IMHO be easier and more consistent.

Copy link
Member Author

Choose a reason for hiding this comment

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

@skurfuerst can you clarify what you mean by "available [...] at runtime"? A parameter of the PrivilegeProvider::getPrivileges() method? But than the ContentGraph would need to pass it

Copy link
Member

Choose a reason for hiding this comment

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

yep, that's what I thought - similar to what we did in the command handlers. But you are right, the projection does not know it yet...

/**
* @internal
*/
final class FakePrivilegeProvider implements PrivilegeProviderInterface
Copy link
Member Author

Choose a reason for hiding this comment

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

Not really a FakePrivilegeProvider but a first implementation to test content stream access


$privileges = Privileges::create();

$userWorkspace = $contentRepository->getWorkspaceFinder()->findOneByWorkspaceOwner($userId->value);
Copy link
Member Author

Choose a reason for hiding this comment

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

This won't necessarily return the actual user workspace, but any shared workspace with the same owner.
Also this won't include base workspaces currently

Copy link
Member

Choose a reason for hiding this comment

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

dangerous code - people without user workspace will automatically be admins. let's discuss :)

Copy link
Member Author

Choose a reason for hiding this comment

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

hehe, good catch. That can't stay like this ofc

Copy link
Member

@skurfuerst skurfuerst left a comment

Choose a reason for hiding this comment

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

really really cool ❤️ ❤️ ❤️ ❤️

$privileges = $this->privilegeProvider->getPrivileges($visibilityConstraints);
if (!$privileges->isContentStreamAllowed($contentStreamId)) {
throw new \RuntimeException(sprintf('No access to content stream "%s"', $contentStreamId->value), 1681306937);
}
Copy link
Member

Choose a reason for hiding this comment

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

I would say yes (if we can, like it is done here). Ofc people could work around this if they manually instanciated ContentSubgraph; but we could never prevent this.

{
private function __construct(
public readonly ?ContentStreamIds $allowedContentStreamIds,
public readonly ?ContentStreamIds $disallowedContentStreamIds,
Copy link
Member

Choose a reason for hiding this comment

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

what is the semantics of having both of these lists? :) I don't fully understand this yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

Me neither :)
But I thought, that might need both. an allow list and a deny list..

Copy link
Member Author

Choose a reason for hiding this comment

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

Looking at this after a break, I think the terms "allowed" and "disallowed" are not helpful.
i.e. what does $privileges->isContentStreamAllowed($contentStreamId) exactly mean?

Also we should re-evaluate whether we really need allow- and deny list

if ($this->contentStreamPrivilege === null) {
return true;
}
return $this->contentStreamPrivilege->allowedContentStreamIds->contain($contentStreamId);
Copy link
Member

Choose a reason for hiding this comment

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

Hm, I am not so sure about this one - IMHO this must be done lazily (otherwise we could under some scenarios have REALLY long lists of contentStreamIds; where only a single one will be relevant here)

Copy link
Member Author

Choose a reason for hiding this comment

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

isn't it just the currently active Content Streams of the user workspace (and its base workspace(s))?

Copy link
Member Author

Choose a reason for hiding this comment

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

rename to sth. that makes clearer that this is about filtering nodes

if (!$privilegeProviderFactory instanceof PrivilegeProviderFactoryInterface) {
throw InvalidConfigurationException::fromMessage('privilegeProvider.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, PrivilegeProviderFactoryInterface::class, get_debug_type($privilegeProviderFactory));
}
return $privilegeProviderFactory->build($contentRepositoryId, $contentRepositorySettings['userIdProvider']['options'] ?? [], $userIdProvider, $contentRepositoryRegistry);
Copy link
Member

Choose a reason for hiding this comment

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

UserIdProvider is fine IMHO, ContentRepositoryRegistry should go away. What about making the $contentRepository available to the provider at runtime? would IMHO be easier and more consistent.


$privileges = Privileges::create();

$userWorkspace = $contentRepository->getWorkspaceFinder()->findOneByWorkspaceOwner($userId->value);
Copy link
Member

Choose a reason for hiding this comment

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

dangerous code - people without user workspace will automatically be admins. let's discuss :)

@bwaidelich bwaidelich mentioned this pull request Oct 13, 2023
5 tasks
@bwaidelich
Copy link
Member Author

bwaidelich commented Oct 24, 2023

A couple of ideas @skurfuerst and I just came up with:

We should change the PrivilegeProviderInterface to a more functional interface like:

interface PrivilegeProviderInterface {

    public function isFetchingOfNodesAllowedInSubgraph(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): PrivilegeResult;

    public function isCommandAllowed(CommandInterface $command): PrivilegeResult;

}

(potentially split into two individual interfaces)
The PrivilegeResult (better name to be found) is basically a boolean + x (e.g. some context on why s.th. is not allowed).

This should make it possible to deal with all 4 security related categories we came up with:

1. Prevent certain commands to be handled

This could be enforced with a single call to the PrivilegeProviderInterface::isCommandAllowed() inside ContentRepository::handle()

Examples

  • Prevent anonymous user from altering nodes
  • Prevent user X from altering nodes in workspace of user Y
  • Allow role X alter only content underneath nodes with a certain tag (e.g. NewsArea)

1a. Determine whether a certain command is allowed

This would not be part of the core itself – Instead the surrounding framework (e.g. Neos) could call their implementation of the isCommandAllowed() method.
That approach might not be feasible – it's up to the implementation to decide

Examples

  • Can Node X moved to Node Y (might be dependent on the node tags)

2. Hide certain nodes depending on the context

This will be enforced via "Subtree Tags" (#4550).
Deciding which tags are allowed in the VisibilityConstraints would not be a responsibility of the CR

Examples

  • Hide disabled nodes in frontend
  • Hide nodes within some "member" area tag in frontend

3. Hide UI elements depending on the context

Like above this would not be a concern of the CR

Examples

  • Hide "disable" button if the corresponding command is not allowed for the authenticated user

@bwaidelich
Copy link
Member Author

Closing in favor of #5298

@bwaidelich bwaidelich closed this Oct 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants