Closed IanMayo closed 4 months ago
Hello @thevahidal - I welcome your thoughts on the 3 possible strategies for delivering this feature.
Actually, it looks like @AbegaM and I have got stuck. We can't quite picture how we statically compile our extension with it being able to call JS code in Soul at run-time. I guess Soul would have to declare/expose some method(s) that is used by the compiler in our JS app - which get resolved at run-time.
Aah, hold on. I may be describing the impossible. The extension
code is being served from our server, but the code itself is being run on the client browser. So, the extension
and soul
are being run in different environments, potentially on different devices. Maybe HTML is the only way for them to communicate. Maybe the route to fixing this is to encrypt/obfuscate the messaging between systems in a way that can't be exploited by an inquisitive user in Chrome Dev Tools.
I'm not familiar with a practice/technology that would allow this. Do you know @thevahidal ?
Hi @IanMayo, It seems that the most effective approach to address the problem at hand is to implement authentication and authorization for Soul. Since both the Soul API and extensions are served from a single Express instance, we can explore the option of disabling the core API and enabling only the extensions. Although this approach may require some additional effort, it has the potential to provide a viable solution.
To begin, I suggest starting with the implementation of a basic authentication system that includes roles such as "admin" and "user." This will help ensure that only authorized individuals can access the Soul API and its extensions. Once the authentication system is in place, we can gradually enhance it to support more sophisticated authentication and authorization mechanisms as needed.
Please let me know what you think.
Hello @thevahidal ,
so - we would add a users
table with a specific schema, right?
Well, it must have:
fields, but could have other domain-specific ones, too.
Hmm, maybe not the "user" and "admin" fields, since the extension
could handle the authorisation, we would just be implementing authentication in this phase.
One thing that I think we should implement before auth is the middleware
extensions. Using middlewares we can enable all current APIs to go through specific steps to check the user's auth status.
Other than that I think having a is_superuser
or is_admin
boolean beside a roles
field which can be defined and assigned by admins is a good structure. For that, we're gonna need another table in charge of storing different types of roles.
By the way, we should also update our rows
endpoints to restrict the calls on the users
endpoint, which only allows is_superuser
users to be able to access them. Also, I think it's good to have a naming convention for the tables that we create, such as this users
table. e.g. They must always start with an underscore _users
.
That sounds good @thevahidal .
Any chance you could draw this up, or present the next level of detail in some way?
I'm happy for @AbegaM and me to start moving it foward.
Sure, @IanMayo, I've prepared a preliminary draft for your consideration.
When Soul boots up, it should check for the existence of a table called _default_permissions
.
_default_permissions
table_name: string
can_create: boolean default false
can_read: boolean default true
can_update: boolean default false
can_delete: boolean default false
For each existing table in the database, a row should be inserted into the _default_permissions
table with the default values. Note that system-defined (private) tables, such as _default_permissions
itself or tables starting with an underscore, should have all four permission fields (can_create
, can_read
, can_update
, can_delete
) set to false.
For example, a row in the _default_permissions
table could look like this:
table_name: "albums"
can_create: false
can_read: true
can_update: true
can_delete: false
In this scenario, calling POST
and GET
requests to /tables/albums/rows
is allowed by any user (even anonymous), while PUT
and DELETE
requests are not allowed. Each field in the row represents the default permission for a specific operation, where false means the permission is not allowed and true means it's allowed. This table allows us to set default roles for a fresh Soul setup.
The _users
table should have the following columns:
_users:
- id: autoincrement int
- username: string (unique)
- first_name: string
- last_name: string
- hashed_password: string
- is_superuser: boolean default false
- roles: many-to-many relationship with the `_roles` table
Users with is_superuser: true are able to access private operations as well.
Now that we have default permissions in place, we can introduce new endpoints for obtaining JWT access tokens and refreshing them. JWT tokens are a good choice for authentication as they enable stateless authentication.
The JWT access token consists of the following claims:
- username: string
- is_superuser: boolean
- roles: array of role names
The "Obtain Access Token" endpoint accepts the username and password as input and returns an access token and a refresh token.
To sign the JWT token, we need to have a new environment variable called SECRET_KEY
.
We can also introduce a "Register" endpoint for user registration, but it can be skipped for now.
To enforce access control based on the default permissions table, an Express.js middleware should be implemented. This middleware checks if the endpoint and method are restricted according to the default permissions table and determines whether the user is allowed to access it or not.
Additionally, we can consider introducing a new type of extension called "middlewares." These middlewares would be similar to extensions in that they allow users to define new endpoints for Soul. Middlewares would help users define global middleware functions that can be applied to multiple routes. We can further discuss this concept in the future.
Roles provide a more sophisticated access control mechanism. While roles are an important aspect, we can focus on completing the design and implementation of the default permissions and authentication first. Once those components are in place, we can dedicate our efforts to working on the roles functionality.
This approach allows us to build a solid foundation for the access control system and progressively enhance it with role-based permissions.
Hey - that's a great piece of work @thevahidal
@AbegaM and I had been trying to design something - but your proposal seems perfectly logical/
Aah, @thevahidal - do you think an MVP is possible without the roles table? Or do you just picture a pre-MVP that has users and authentication, then we add roles for actual MVP?
@IanMayo, I believe prioritizing the completion of user management and authentication before diving into role implementation is a good decision. Treating it as an MVP allows us to establish a foundation for restricting certain operations on specific tables. However, implementing Role-based Access Control would undoubtedly take the system to the next level in terms of security and access management.
Also I wanted to give you a shoutout for your fantastic design! I couldn't help but notice how much synergy there is between our ideas. I truly believe that by blending elements from both of our designs, we can create something truly amazing.
I started a discussion in the users and roles issue (#4) related to how Soul could be installed, with access only allowed to a Soul Extension located within the application itself.
Then the installed extension can access the full REST API, but no other external software can.
This doesn't handle the
users and roles
feature requirement, it just avoids it - since the installed extension can handle authentication and authorisation.An example use case for this feature would be to serve a
react-admin
application with secure access to a private SQLite backend provided bysoul
.