-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Security - Ability to create custom permission annotation #39663
Comments
You added a link to a Zulip discussion, please make sure the description of the issue is comprehensive and doesn't require accessing Zulip This message is automatically generated by a bot. |
/cc @sberyozkin (security) |
I agree. I'm not sure what stops us from doing that, but I'll add this issue on my list. |
I think this is similar to |
This looks like a good idea. Although, I still think permissions such as "canwrite" that do not have access to what they can write on are fantasy permissions that do not exist in most cases. In most cases permissions need to know who the current user is, and what objects they're expected to work on, in addition to what operation they will do on the object. Here's a more realistic example of how we could define custom permissions that work on real objects, in a way that makes them composable (I've intentionally cut the permission logic into smaller units so that different endpoints can compose them finer-grained): package model.permissions;
import java.util.List;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;
import io.quarkiverse.renarde.security.ControllerWithUser;
import io.quarkus.arc.Arc;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.vertx.ext.web.RoutingContext;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.ws.rs.POST;
// The entity representing the current user
@Entity
public class User extends PanacheEntity {
public boolean isAdmin;
public String username;
}
// A project belongs to an owner, has a unique name
@Entity
class Project extends PanacheEntity {
@ManyToOne
public User owner;
public String name;
@OneToMany
public List<Contributor> contributors;
@OneToMany
public List<Team> teams;
public static Project findByName(String projectName) {
return find("name", projectName).singleResult();
}
}
enum ContributionType {
Admin, Read, Write;
}
// A Contributor has specific permissions on a project
@Entity
class Contributor extends PanacheEntity {
@ManyToOne
public User owner;
public ContributionType type;
@ManyToOne
public Project project;
}
// A team aggregates people with a single permission on a project
@Entity
class Team extends PanacheEntity {
@ManyToOne
public ContributionType type;
@ManyToOne
public Project project;
@OneToMany
public List<User> members;
}
// The main permission type to extend, single method
interface QuarkusPermission {
boolean isAllowed();
}
// Allows access to the routing context (simplistic)
interface QuarkusRoutedPermission extends QuarkusPermission {
default RoutingContext getRoutingContext() {
return Arc.container().instance(RoutingContext.class).get();
}
}
// Quarkus user's main permission type, binds permissions to their model of the logged in user
interface MyPermission extends QuarkusRoutedPermission {
default User getUser() {
return Arc.container().instance(User.class).get();
}
}
// Quarkus user's root for permissions around projects: needs a user and project
interface ProjectPermission extends MyPermission {
default Project getProject() {
String projectName = getRoutingContext().pathParam("projectName");
if(projectName == null)
return null;
return Project.findByName(projectName);
}
}
// Current user is admin
interface IsAdminPermission extends MyPermission {
@Override
default boolean isAllowed() {
return getUser().isAdmin;
}
}
// Current user is owner of project
interface IsProjectOwnerPermission extends ProjectPermission {
@Override
default boolean isAllowed() {
Project p = getProject();
return p != null && p.owner == getUser();
}
}
// Current user is project admin of project
interface IsProjectAdminPermission extends ProjectPermission {
@Override
default boolean isAllowed() {
Project p = getProject();
if(p == null) {
return false;
}
User user = getUser();
for (Contributor contributor : p.contributors) {
if(contributor.owner == user && contributor.type == ContributionType.Admin) {
return true;
}
}
for (Team team : p.teams) {
if(team.type == ContributionType.Admin) {
for (User member : team.members) {
if(member == user) {
return true;
}
}
}
}
return false;
}
}
// Current user can rename project
interface CanRenameProjectPermission extends IsAdminPermission, IsProjectOwnerPermission, IsProjectAdminPermission {
@Override
default boolean isAllowed() {
return IsAdminPermission.super.isAllowed() || IsProjectOwnerPermission.super.isAllowed() || IsProjectAdminPermission.super.isAllowed();
}
}
// Annotations for those permissions
@interface CustomPermission{
Class<? extends QuarkusPermission> value();
}
@CustomPermission(IsAdminPermission.class)
@interface IsAdmin {}
@CustomPermission(CanRenameProjectPermission.class)
@interface CanRenameProject {}
class SecureController extends ControllerWithUser<User> {
// public: no permission required
Project readProject(@RestPath String projectName) {
Project project = Project.findByName(projectName);
notFoundIfNull(project);
return project;
}
@CanRenameProject
@POST
void renameProject(@RestPath String projectName, @RestForm String newName) {
Project project = Project.findByName(projectName);
// assume we have permission, since it's annotated, also implies the project must already exist, so no need to check for null
project.name = newName;
}
} There is the problem of figuring out what to do in case for example the project does not exist, which means the permission can't be granted, so we'd get a 401 instead of a 404 that we would be able to do if we did get into the endpoint to check for this before we checked for permissions. Now, the bigger problem here is the implementation of The proposed implementation using We could add hooks into Quarkus REST to make it easy to access the endpoint parameters (as they would be passed to the endpoint, so after deserialisation) but that would mean invoking the security check after deserialisation and filters, while ATM I suppose it's checked before. But that would be more useful for security checks. Closer to what one could write in code checks. Well, it's pretty much required anyway, because we'll need a transaction open to do anything useful, and that's done by the CDI interceptor which currently runs after deserialisation (not sure about filters, have to check). Anyway. Lots of ways to over-engineer security checks if we want annotations. In my experience, doing checks in code is pretty much always required due to the complexity of expressing this in annotations. |
@FroMage man, you really changed the game :-) I believe that what @zZHorizonZz and @jasoncsmith7 described is simply support for meta annotation with However I don't want to ignore your comment as you put a lot of work into it and there are parts I do agree, not sure what to do, don't want to discard what I am doing as at least 2 users asked for it and meta-annotations support for
In custom
In custom Line 16 in 155093d
All security checks except for
I don't have test for this, mostly because it would require started database.
I think we need separate issue for your proposal, unless I completely misunderstood issue description. There is GitHub feature "Reference in new issue", it takes just few second, would you mind creating one from your comment above, please? Unless you disagree that meta-annotation support should be implemented. Thanks |
Yeah, this original issue is probably fine. I'm very skeptical that real applications can get by with just this, though. In my experience, string roles without being able to reason about the target are never useful. But people ask for this, for some reason, so there's probably value.
It works, for sure, but it doesn't contain path parameters that have been defined by Quarkus REST. It would only contain those it knows about when defined as Vert.x routes, which is not the case in most applications. So it's not useful for Quarkus REST apps.
Oh, I did not realise that. This is great. How do you access them, then?
Yeah, but that's pretty crucial to know.
It could be moved to a new issue, I'm not saying we should do this, it's just a first draft of a more realistic permissions framework that actually can compete with real-life security checks in code. Mostly done to show you how security checks in code, using endpoint parameters and the real DB model would look like if we tried real hard to put it in annotations, and to illustrate that string permissions can't possibly be enough in applications where security requires the DB model. |
Moved to #43238 |
Just shortly as I am in hurry today:
For example, here is test for Quarkus REST: Line 52 in f8f7f00
It accepts Line 14 in f8f7f00
You can pass there any endpoint method parameter. Today I used Jakarta REST annotations like
Got it. thank you. I agree this |
Description
Inspired by a conversation on Zulip: https://quarkusio.zulipchat.com/#narrow/stream/187030-users/topic/.E2.9C.94.20grpc.20-.20current.20grpc.20message.20in.20auth, I propose a new feature for custom permission annotation for gRPC and REST.
Concept:
Allow creation of custom permission annotations, themselves annotated with @PermissionsAllowed.
You would be able to define default permission values within these annotations and other values as like for example value itself.
Optionally, it would be nice to have passing custom parameters to the custom permission class from custom permission (Explained in implementation), if specified.
Implementation ideas
As suggested in conversation you would be able to do this:
Or you would be able to do this:
You would be then able to do this:
The text was updated successfully, but these errors were encountered: