Skip to content

feat: Introduce Permission Sets #2002

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

Merged
merged 12 commits into from
May 21, 2025
Merged

feat: Introduce Permission Sets #2002

merged 12 commits into from
May 21, 2025

Conversation

jirijakes
Copy link
Collaborator

@jirijakes jirijakes commented May 15, 2025

Notes and hightlights for reviewers:

  • original system of permissions is still in use, only change is location where superuser user and role are created (newly in async fn bootstrap_access_control)
  • Permission Set entity and Role entity are not used, although their tables already get populated
  • permissions sets together with action and object names are collected via LanaAction::action_descriptions()
  • unresolved review comments and unfinished tasks will be moved to next PR (they are related to new entities)

Permission Sets are predefined named sets of permissions that can be assigned to Roles by administrators. Their purpose is to shield administrators from the underworld of granular and dynamic permissions.

Tasks:

  • Move User under CoreUser (f3ea97e)
    • CoreUser will now contain Role, User and AccessGroup entities
    • unfortunate consequence: app.users().users().create_user()
      • how about app.access().users(), app.access().roles(), app.access().permission_sets()?
      • another alternative: app.core_users().users()
        • but clash with accounting() vs. core_accounting()
      • also an option is to put user use cases right on app.users() but that would imply a hierarchy of the entities that does otherwise exist
  • Introduce basic entity Permission Set (c020ec3)
    • name alternatives: Permission Set, Permission Group, Permission Bundle or other permutations of the words (renamed from originally proposed Access Group)
    • see definition of Permission Set in core/user/src/permission_set/mod.rs
    • Permission and Permissions types are placeholders for something else, unknown yet
    • Permission Sets do not necessarily have to be entities; they remain unchanged during the life of the application and can be statically defined in the code, maybe even in a const context
  • Allow assign Permission Sets to Roles (e208526)
    • will require some modifications to Role entity
  • Wire everything with Casbin
    • will use Role ID and Permission Set ID as role identifier (instead of names)
    • will use casbin's standard role hierarchy
  • Add bootstrapping (37ff646)
    • superuser role and superuser user created during bootstrap
    • all Permission Sets will be created during bootstrap
      • may be gradual process
      • perhaps we start slowly by introducing a few reasonable Permission Sets and a catch-all for remaining permissions
    • other predefined roles will be created in seed (e. g. admin, bank manager)
  • Adapt seeding
    • create predefined roles with permission sets
  • Try to find a bullet-proof way to collect all permissions (20770d7)
    • in const context generates hierarchy of modules, objects and actions
    • uses strum::EnumCount and Rust arrays to check on compile time that we provide correct number of items
    • some compile-time checks so when we add object or action, compiler will tell us that we forgot something
    • unfortunately tonnes of new boilerplate; but it is quite copy&paste friendly
    • this hierarchy is used to generate all Permission Sets
  • Define Permission Sets (and supporting types) (646ff84)
    • at first only a few meaningful permission sets + one catch-all
    • at first permission sets mirror currently used roles: superuser, admin, bank manager, accountant
    • permission sets defined for reading and writing per module
    • static strings at the moment, until we figure out better way of representing them
  • Expose entire hierarchy for inspection
    • read-only access to roles, permission sets and permissions for inspection and audit
    • could be also used in a test verifying that every permission is used in some permission set
  • Rename CoreUser to CoreAccess

Tasks will be updated on the go. The order does not have to be followed, some tasks may be done in parallel and some tasks may be transferred to a distinct PR. Feel free to comment anytime.

Copy link

github-actions bot commented May 15, 2025

Manuals preview: Link to Manuals

@@ -25,6 +25,7 @@ pub async fn user_id_from_authentication_id(
};

match app
.users()
Copy link
Member

Choose a reason for hiding this comment

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

this looks weird - probably should be core_users()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I also find it unpleasant. How would you find renaming CoreUser into CoreAccess as a module for access control, i. e. users, roles, permission sets etc.

core_users() would work, too.

Copy link
Member

Choose a reason for hiding this comment

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

core/access works for me

@jirijakes jirijakes changed the title feat: Introduce Access Groups feat: Introduce Permission Sets May 15, 2025
@jirijakes jirijakes force-pushed the access-group branch 3 times, most recently from 82b0787 to 881fc48 Compare May 19, 2025 11:24
let permission_sets = self.permission_sets().find_all(permission_set_ids).await?;

for (permission_set_id, _) in permission_sets {
let _ = role.add_permission_set(permission_set_id, audit_info.clone());
Copy link
Member

Choose a reason for hiding this comment

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

needs idempotency check?

.await?;

self.users()
.bootstrap_superuser(email, superuser_role.name, &mut db)
Copy link
Member

Choose a reason for hiding this comment

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

should probably pass superuser_role.id?

Copy link
Member

Choose a reason for hiding this comment

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

Also I think we have standardized on having db always be the first arg.

/// Generates Permission Sets based on provided hierarchy of modules and
/// returns all existing Permission Sets. For use during application bootstrap.
//
// Warning: think thrice if you need to make the method more visible.
Copy link
Member

Choose a reason for hiding this comment

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

Why this warning here? We should always use the minimal visibility on every fn in the entire code base.

self.repo.find_by_id(&role_id).await
}

pub async fn list(&self, sub: &<Audit as AuditSvc>::Subject) -> Result<Vec<Role>, RoleError> {
Copy link
Member

Choose a reason for hiding this comment

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

We always want to expose the cursor.

.entities)
}

pub async fn update(&self, role: &mut Role) -> Result<(), RoleError> {
Copy link
Member

Choose a reason for hiding this comment

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

Why would update be exposed? role mutation use cases should be in this file.

.await
}

pub async fn create_role_with_permissions_sets(
Copy link
Member

Choose a reason for hiding this comment

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

isn't create_role the entry point? Does this need to be pub?

/// Creates a role with name “superuser” that will have all given permission sets.
/// Used for bootstrapping the application.
//
// Warning: think thrice if you need to make the method more visible.
Copy link
Member

Choose a reason for hiding this comment

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

Why warning?

///
/// To obtain qualified name of the action and its related object, two
/// segments of the path need to be known (i. e. `P = (String, String)`).
pub struct Action<P = (String, String)> {
Copy link
Member

Choose a reason for hiding this comment

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

ActionDescription<> with aliases NoPath | EntityPath | ModuleAndEntityPath

Copy link
Member

Choose a reason for hiding this comment

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

Can we try to use &static str?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Aliases done but &'static str have not tried yet.

@jirijakes jirijakes force-pushed the access-group branch 4 times, most recently from 646ff84 to eab51ca Compare May 20, 2025 07:42
@jirijakes jirijakes marked this pull request as ready for review May 20, 2025 08:48
jirijakes added 3 commits May 20, 2025 15:54
This has an unfortunate consequence in modules CoreUser and Users
both being accessed via `users()`, for example
`app.users().users().create_user()`. Better naming would be
desirable. For example CoreAccess and `app.access().users()`?
@jirijakes
Copy link
Collaborator Author

Squashed Vaibhav's and Justin's changes into one commit for easier review. Otherwise no change.

One question – with this module-read/write permission sets, some granularity and nuances that existed before are now lost. For example, previously 'assign role to user' and 'revoke role from user' could be done only by a dedicated role (superuser); a role that could create users could not assign roles to them. Now it is possible because they all share the same 'user write' permission set.

The question is, how precise and intentional was the previous role-based system? Do we have to preserve all these nuances? Does it have to be precise from the beginning or will we do it gradually, as needed?

@jirijakes jirijakes merged commit eec4916 into main May 21, 2025
17 checks passed
@jirijakes jirijakes deleted the access-group branch May 21, 2025 07:51
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.

2 participants