SQL query inner join tables, print to HTML <select> tag

旧巷老猫 提交于 2019-12-11 07:45:34

问题


I have been tasked with updating an old application and ran into a head scratch-er. The application runs on Railo(ColdFusion) with a MySQL 5 database.

The application is using Roles based authentication to manage users. The previous developer appeared to have no code to modify/add/delete users and just modified the database columns directly. I have taken care of the add/delete user part, no problem. The modify part is where I stumble.

Below is the basic table layout for Roles and Members.

MEMBERS TABLE
id | username | role
 1 | user1    | Administrator,User,Group1,Group2
 2 | user2    | User,Developer
 3 | user3    | SuperUser,Group1,Group2
 4 | user4    | SuperUser,Developer
 5 | user5    | Guest

ROLES TABLE
role_id | role_name
      1 | Administrator
      2 | Developer
      3 | SuperUser
      4 | Guest
      5 | User
      6 | Group1
      7 | Group2

To modify a user's roles, my first thought was to use a simple HTML select tag and a couple CFQUERY. Something similar to the code below.

<select name="role" multiple="multiple" size="7">
<cfoutput>
<option value="#GetRole.role_name#" <cfif GetRole.role_name = GetUser.role>checked</cfif>>#GetRole.role_name#</option>
</cfoutput>
</select>
<CFQUERY NAME="GetRole" DATASOURCE="#request.dataSource#">
  SELECT role_name
  FROM roles
</CFQUERY>

<CFQUERY NAME="GetUser" DATASOURCE="#request.dataSource#">
  SELECT a.id, a.username, a.role, b.role_name
  FROM members a
  LEFT JOIN roles b ON b.role_name = a.role
  WHERE a.id = #FORM.id#
</CFQUERY>

The select tag would pull from GetRole and populate all available roles. The GetUser query would find the current roles the user is a member of from the list in the members table. Then I would simply use the "checked" flag in the select tag. That is where I hit a wall. No matter what path I take I keep running into array to list issues.

When I add a new user I simply use" role = '#ListQualify(FORM.role,"")#' and that takes all the roles in my select tag, adds a comma, and spits them into the members.role column.

I even tried going step by step and creating an array:

<cfset roleArray = ArrayNew(1)>

<cfloop query = "GetUser">
<cfset temp = ArrayAppend(roleArray, "#role#")>
</cfloop>

<cfset roleList = ArrayToList(roleArray, ",")>

I tried doing a SQL FIND_IN_SET() as well.

No matter what I try I get the dreaded array to Boolean errors.

Suggestions? Should I try a different approach than an HTML select tag? Is there something I overlooked? Any help would be most appreciated.

Hopefully I posted this question well, only my second time to post a question here. :D

-------------------- UPDATE --------------------

I wanted to add that the application uses IsUserInRole() throughout the site to check if a user has access to certain areas or not. For example:

 <cfif IsUserInRole("Administrator")>
 Do stuff here as an administrator.
 </cfif>

It is also used with multiple groups, and ColdFusion automatically finds each group seperated by commas:

<cfif IsUserInRole("Administrator,Group1,Group2")>

Is there a better way for ColdFusion to manage Roles?

-------------------- UPDATE #2 --------------------

I discovered the original developer used CFLOGIN to set the roles for users. I have heard horror stories about CFLOGIN, so should it be nixed in favour of something better? Set a user's role in session variables instead perhaps?

<cfquery name="qryGetUserDetails" datasource="#request.datasource#">
SELECT *
FROM members
WHERE username = <cfqueryparam cfsqltype="cf_sql_varchar" value='#Trim(FORM.username)#'>
</cfquery>
<cflogin>
  <cfloginuser name="#FORM.username#" password="#FORM.password#" roles="#qryGetUserDetails.role#">
</cflogin>

Notice the cfqueryparam? :D I applied what I learned from you guys quickly! :D


回答1:


(Too long for comments)

I even tried going step by step and creating an array:

Ignoring db structure for a moment, creating an array is a waste of time :) If the roles are stored as a list, then all you are doing is taking the list, creating a single element array and converting it back into the same list again. So get rid of the array. It is does not serve any purpose.

Just loop through the getRole query and use list functions as braketsage suggested. Dan makes a good point about checkboxes, but I will use your original code to better illustrate:

<select name="role" multiple="multiple" size="7">
   <cfoutput query="getRole">
      <option value="#getRole.role_name#" 
        <!--- if the current role is found in the list of user roles --->
        <cfif listFindNoCase(getUser.ListOfRoles, getRole.role_name)>
           selected
        </cfif>>
        #GetRole.role_name#
       </option>
   </cfoutput>
</select>

Having said that, the real problem is your database structure. Storing lists is one of those things that seems like it will make life easier, but almost always creates more problems than it solves. For example, using the current structure - how you would you identify all users that have the role of "Administrators" and "Guest" but not "SuperUser"?

While there are some kludgey techniques to get around some of the inherent limitations of storing lists, you should really change the table structure if at all possible. Lists are more prone to data integrity issues. Also, due to the reliance on string functions, it frequently requires convoluted SQL queries that are unable to utilize db indexes, and consequently do not scale well.

As I mentioned in the comments, a better structure is to create a third table: MemberRole. Store each memberID + roleID combination as a separate row. That structure would offer greater flexibility and reliability. See braketsage's answer for an example. Though the "Edit user" query joins could be simplified a bit. I removed some of the logic for clarity. However, as braketsage noted in the original post, you may want to add additional filters to limit which roles the current user can assign - for security reasons. Otherwise, any user could assign any permissions.

Note: I added a boolean flag that I like to use in my apps. Using an OUTER JOIN and CASE statement you can create a boolean column called IsAssigned that indicates whether or not each role is assigned to the selected user. That flag comes in handy for pre-selecting list items (or checkboxes) on the edit screen.

SELECT  ur.roleID
        , ur.roleTitle
        , ur.UserID
        , ur.UserName
        , CASE WHEN p.UserID IS NOT NULL THEN 1 ELSE 0 END AS IsAssigned
FROM   (
          SELECT u.UserID
                 , u.UserName
                 , r.RoleID
                 , r.RoleTitle
          FROM   Users u CROSS JOIN Roles r
          WHERE  u.UserID = <cfqueryparam value="#FORM.id#" cfsqltype="cf_sql_integer">

       ) 
       ur LEFT JOIN Permissions p
                ON p.RoleID = ur.RoleID
                AND p.UserID = ur.UserID

NB: Be sure to read up on CROSS JOIN

That said, for readability I often just run two queries: one to get the user information and another to get the assigned roles. It is an extra db call, but slightly less data pulled back, so the extra query is not too big a deal.




回答2:


A sample structure of a proper role system

I just put such a system into an application as Leigh Describes. Unfortunately, while it is built for Railo, it is built on SQL Server, so you might need to modify the sql.

Users
UserID | Username ....

Roles
RoleID | RoleTitle ...

Permissions
UserID | RoleID

My edit user query looks like this

  • You'll notice the long <cfqueryparam> tags. For both Railo and ACF, they are a critical line of defense against sql injections.

  • Edit: Updated with a cleaner query query written by Leigh. It is cleaner than my version that served the same purpose.

  • I did add line 6 (and r.roleID in (select distinct pm.roleID from permissions pm where userID = <cfqueryparam cfsqltype="int" value="#session.userID#">)). This ensures that the only roles that the moderating user can effect are roles that they themselves are in.

-

/* pull user details and role in one query */
SELECT  ur.*, CASE WHEN p.UserID IS NOT NULL THEN 1 ELSE 0 END AS HasRole
FROM   (SELECT u.*, r.RoleID, r.RoleTitle
        FROM   Users u CROSS JOIN Roles r
        WHERE  u.UserID = <cfqueryparam cfsqltype="int" value="#url.userID#">
and r.roleID in (select distinct pm.roleID from permissions pm where userID = <cfqueryparam cfsqltype="int" value="#session.userID#">)) ur
LEFT JOIN Permissions p
    ON p.RoleID = ur.RoleID
    AND p.UserID = ur.UserID;

/* get permissions separately */
SELECT r.RoleID, r.RoleTitle, CASE WHEN p.RoleID IS NOT NULL THEN 1 ELSE 0 END AS HasRole
FROM   Roles r LEFT JOIN Permissions p
        ON p.RoleID = r.RoleID
        AND p.UserID = <cfqueryparam cfsqltype="int" value="#url.userID#">

In the form itself, I have this code.

<cfoutput query="GetUser" group="username">
...other form elements...
<input type="checkbox" name="roleschanged" value="1" onchange="if ($(this).prop('checked')) { $('.rolesCheck').removeAttr('disabled'); } else { $('.rolesCheck').attr('disabled','false'); }"> Editing Roles?<br><br>
<cfoutput group="roleID">
  <input type="checkbox" name="roleID" value="#roleID#" class="rolesCheck" disabled #(hasrole eq 1 ? "checked" : "")#> #RoleTitle#<br>
</cfoutput>
..
</cfoutput>

The checkboxes load disabled, but they're checked if the user is part of that role, and unchecked if not. The roleschanged checkbox can enable them, so roles can be changed, and also serve the key purpose on the processing page to signify user roles have been edited.

In the form processing code, I have.

<cfif isDefined("form.rolesChanged") and form.rolesChanged eq 1><cfquery datasource="#request.dsn#">
  delete from Permissions
   where userID = <cfqueryparam cfsqltype="int" value="#val(url.userID)#">
     and roleID in (select distinct pm.roleID from permissions pm where userID = <cfqueryparam cfsqltype="int" value="#session.userID#">)
</cfquery>

<cfloop list="#form.roleID#" index="i">
  <cfquery datasource="#request.dsn#">
    insert into Permissions(userID,roleID)
     values(<cfqueryparam cfsqltype="int" value="#val(url.userID)#">,<cfqueryparam cfsqltype="int" value="#val(i)#">)
  </cfquery>
</cfloop></cfif>

Form.roleschanged's ultimate purpose is so that these queries aren't run every time the page is run regardless of inaction on roles.

Though that is the ultimate purpose, it does allow the moderating user the ability to skip changing the data without resetting the rest of the form.

To visually represent that functionality is why the checkboxes are disabled on pageload.

My roles table also has an Area field which is an absolute path to areas they can edit, like...

RoleID, RoleTitle, RoleArea
1       Pages      /wwwroot/cpanel/cms/
2       News       /wwwroot/cpanel/news/

Whenever a user tries to access any folder within cpanel, I do a check and if they don't have a role matching the folder they're trying to access, they get punted to logout.

I can (but don't) also use this table to build admin links, so that admins only ever see links to areas they can use. (I pull links from my cms, but my cms entries also have links to the roles table. The only difference is that the more basic route would link to index.cfms listing what a user could do within each folder, whereas the method I actually use allows me to provide direct links.. not really a notable gain).



来源:https://stackoverflow.com/questions/26227103/sql-query-inner-join-tables-print-to-html-select-tag

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!