Database Schema for Claims Authentication - sql

On a database I have the tables: USERS, USERS_PROFILES, USERS_CLAIMS.
create table dbo.USERS
(
Id int identity not null,
Username nvarchar (120) not null,
Email nvarchar (120) not null
);
create table dbo.USERS_PROFILES
(
Id int not null,
[Name] nvarchar (80) not null
);
create table dbo.USERS_CLAIMS
(
Id int not null,
[Type] nvarchar (200) not null,
Value nvarchar (200) not null,
);
I am using Claims authorization. When a user signs up and Identity is created.
The identity contains claims and each claim has a type and a value:
UsernameType > Username from USERS
EmailType > Email from USERS
NameType > Name from USERS_PROFILES
RoleType > Directly from USERS_CLAIMS
So I am creating the Identity from many columns in 3 tables.
I ended up with this because I migrated to Claims Authentication.
QUESTION
Should I move the Username, Email and Name to USERS_CLAIMS?
The USERS_PROFILES table would disappear ...
And USERS table would contain only info like "UserId, LastLoginDate, CreatedDate, ..."
If I want get a user by username I would just get the Claim of type username ...
If I want to sign in the user I just get all claims and create the identity.
So the Identity Model is much similar to the SQL tables.
Does this make sense? How would you design the tables?
Thank You,
Miguel

You are creating a key value store. They are a nightmare to query in SQL. Consider the difficulty of querying user attributes by a value on the USER_CLAIMS table. Example:
-- Users with name and email by username
SELECT p.ID, p.Username, p.Name, p.Email, u.LastLoggedIN
FROM USER_PROFILES p
INNER JOIN Users u on p.ID = u.ID
WHERE p.ID = #UserID
-- Users with name and email by username with a claims table
-- Does not specify whether there is only one email, so this could return multiple
-- rows for a single user.
SELECT p.ID, cUName.Value as Username, cName.Value as Name, cEMail.Value as Email, u.LastLoggedIN
FROM Users u
LEFT OUTER JOIN USER_CLAIMS cName ON u.ID = cName.ID and cName.[Type] = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
LEFT OUTER JOIN USER_CLAIMS cUName ON u.ID = cUName.ID and cUName.[Type] = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier'
LEFT OUTER JOIN USER_CLAIMS cEmail ON u.ID = cEmail.ID and cEmail.[Type] = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email'
WHERE p.ID = #UserID

Can a user have multiple profiles? If not, there is no need for the "USERS_PROFILES" table. Keep the "Username" and "Email" columns on the "USERS" table. If you put them on the "USERS_CLAIMS" table, you would be storing redundant information anytime a user files a claim.
I am not sure what kind of tracking you'd like to have for your users, but I would recommend having a separate table that tracks when a user signs in. Something like this:
CREATE TABLE USERS_LOG (user_id INT, log_in DATETIME);
You can then get rid of the "LastLoginDate" on your "USERS" table and do a join to get the last time the user signed in. It'll give you more ways to track your users and you won't be creating blocks on your "USERS" table by updating it constantly.

Related

How to verify table update and migrate data from another table - postgresql

I have following two tables in my potgres database with each type.
user
userid | bigint (PK) NOT NULL
username | character varying(255)
businessname | character varying(255)
inbox
messageid | bigint (PK) NOT NULL
username | character varying(255)
businessname | character varying(255)
What i wanna achieve here is i want to add a new field called userRefId to inbox table and migrate data on user table's userid data into that where each username and businessname match in both tables.
These are the queries i use to do that.
ALTER TABLE inbox ADD userRefId bigint;
UPDATE inbox
SET userRefId = u.userid
from "user" u
WHERE u.username = inbox.username
AND u.businessname = inbox.businessname;
Now i want to verify the data has been migrated correctly. what are the approaches i can take to achieve this? (Note : the username on inbox can be null)
Would this be good enough to verification?
Result of select count(*) from inbox where username is not null; being equal to
select count(userRefId) from inbox;
Is the data transferred correctly? First, the update looks correct, so you don't really need to worry.
You can get all rows in consumer_inbox where the user names don't match
select ci.*. -- or count(*)
from consumer_inbox ci
where not exists (select 1
from user u
where ci.userRefId = u.userId
);
This doesn't mean that the update didn't work. Just that the values in consumer_inbox have no matches.
Under the circumstances of your code, this is equivalent to:
select ci.*
from consumer_inbox ci
where userId is null;
Although this would not pick up a userId set to a non-matching record (cosmic rays, anyone?).
You can also validate the additional fields used for matching:
select ci.*. -- or count(*)
from consumer_inbox ci
where not exists (select 1
from user u
where ci.userRefId = u.userId and
ci.username = u.username and
ci.businessname = u.businessname
);
However, all this checking seems unnecessary, unless you have trigger on the tables or known non-matched records.

How do I filter data between multiple columns into new columns after INNER JOINS?

Brand new to SQL, so apologies that I don't really know how to word the question or find an existing answer. Let me explain further. I'm creating a Chat app for fun with a DM system. I have a table (dms_history) setup that has a row gen. for every new distinct chat between 2 users w/ the last DM being sent between the 2 users. eg:
CREATE TABLE users (
uid SERIAL PRIMARY KEY,
pid VARCHAR(40),
uname VARCHAR(50) NOT NULL UNIQUE,
email valid_email,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE dms (
dmid SERIAL PRIMARY KEY,
uid INT NOT NULL REFERENCES users(uid),
recip INT NOT NULL REFERENCES users(uid),
msg TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- TABLE I'M TALKING ABOUT
CREATE TABLE dms_history (
user1 INT REFERENCES users(uid),
user2 INT REFERENCES users(uid),
last_dm INT REFERENCES dms(dmid),
PRIMARY KEY(user1, user2),
CHECK (user1 < user2)
);
Say I'm trying to get all the data w/ joins on this table (dms_history) for client user uid#2. eg:
SELECT
u1.uid as u1id,
u1.uname as u1name,
u2.uid as u2id,
u2.uname as u2name,
dms.dmid,
dms.msg
FROM dms_history h
INNER JOIN users u1 ON h.user1 = u1.uid
INNER JOIN users u2 ON h.user2 = u2.uid
INNER JOIN dms ON h.last_dm = dms.dmid
WHERE u1.uid = 2 OR u2.uid = 2;
That query I thought of is close to what I want, but what I really want is only to show the user opposite of the client user being queried (so NOT user uid#2 alice) along w/ the last message sent. How do I filter or UNION? those columns into a new column. Picture to explain what I want:
Think of Twitter DMs and how it shows the users you're DMing and a snippet of the last message sent.
Apologies if I did a poor job explaining, it's not my strong suit.
After doing some more reading the past few hours I found I could use subqueries with SELECTs and UNIONs to achieve what I was after. Not sure how efficient it is, but it works.
WITH chats AS (SELECT * FROM dms_history h WHERE h.user1 = 2 OR h.user2 = 2)
SELECT
chats2.user1 as recip_id,
u.uname as recip_uname,
chats2.last_dm as dmid,
dms.msg,
dms.created_at
FROM (
SELECT chats.user1, chats.last_dm FROM chats
UNION
SELECT chats.user2, chats.last_dm FROM chats
) AS chats2
INNER JOIN users u ON chats2.user1 = u.uid
INNER JOIN dms ON chats2.last_dm = dms.dmid
WHERE chats2.user1 != 2;

How to replace a row from a table with another row from another table?

I have the following code on SQL:
select u.openid, u.screenname, svd.user_name
from gw_svd_prefix_assignment svd
join user_ u
on u.screenname = svd.USER_NAME;
now it will show three rows, screenname, user_name and openID. The screenname and user_name are the exact same, that's why I joined them, but I want to change the user_name to the value of openID which is different.
How can I do so?
EDIT Below is an example:
OPENID SCREENNAME USER_NAME
==========================================
Smith.A Smith.Alan Smith.Alan
Someone.J Someone.Juan Someone.Juan
Foo.V Foo.Vallery Foo.Vallery
Hee.L Hee.Lee Hee.Lee
I want the table to look like:
OPENID SCREENNAME USER_NAME
==========================================
Smith.A Smith.Alan Smith.A
Someone.J Someone.Juan Someone.J
Foo.V Foo.Vallery Foo.V
Hee.L Hee.Lee Hee.L
so I want to replace the values in User_Name with the corresponding ones from OPENID
Try this
select u.openid, u.screenname, u.openid user_name
from gw_svd_prefix_assignment svd
join user_ u
on u.screenname = svd.USER_NAME;
If screenname is not unique in the user_ table, then the requirement doesn't make sense (which row in the user_ table should be used to update a row in svd, if the screenname is not unique?)
If screenname is unique, then the update can be done very easily as shown below, but this requires a UNIQUE constraint on screenname (or a Primary Key constraint). If a constraint doesn't exist currently, it can be added with an ALTER TABLE ... ADD CONSTRAINT ... statement.
update ( select user_name, openid, screenname
from svd join usr on svd.user_name = usr.screenname )
set user_name = openid;

Select from many to many table query

I have some tables:
Sessions
SessionID int PK
Created datetime
SiteId int FK
Tracking_Parameters
ParamID int PK
ParamName nvarchar
Session_Custom_Tracking
SessionID int FK
ParamID int FK
ParamValue nvarchar
Site_Custom_Parameters
SiteID int FK
ParamID int FK
ParamKey nvarchar
Sessions: Contains the unique session id for a visitor and the time they entered the site.
Tracking_Parameters: Contains a list of things that you may want to track on a site (i.e. Email Open, Email Click, Article Viewed, etc.)
Site_Custom_Parameters: For a particular site (table not shown), declares the key value for a Tracking_Parameter (i.e. the key to look for in a query string or route)
Session_Custom_Tracking: The link between a session and a tracking parameter and also contains the value for the parameter's key when it was found by my application.
Question:
I want to select session id's where for these particular sessions, there is a record in the Session_Custom_Tracking for two different ParamID's. I want to find sessions where a user both opened an email (paramid 1) and clicked (paramid 3) a link in that email.
You can join to the same table twice:
SELECT S.SessionID
FROM Sessions AS S
JOIN Session_Custom_Tracking AS SCT1
ON SCT1.SessionID = S.SessionID
AND SCT1.ParamID = 1
JOIN Session_Custom_Tracking AS SCT2
ON SCT2.SessionID = S.SessionID
AND SCT2.ParamID = 3
An alteranative that might be easier to read (because it more closely matches the way you describe the problem) is to use WHERE EXISTS:
SELECT S.SessionID
FROM Sessions AS S
WHERE EXISTS
(
SELECT *
FROM Session_Custom_Tracking AS SCT1
WHERE SCT1.SessionID = S.SessionID
AND SCT1.ParamID = 1
)
AND EXISTS
(
SELECT *
FROM Session_Custom_Tracking AS SCT2
WHERE SCT2.SessionID = S.SessionID
AND SCT2.ParamID = 3
)

SQL Query causing me lack of sleep

So I am revising for an exam and have struck a big rock in the SQL river (or waste ground)
I made the following tables and inserted the following data:
create table Permissions
(
fileName VARCHAR(40),
userID VARCHAR (16),
type VARCHAR(10),
startdate DATE,
duration NUMBER (5),
constraint Pri_key PRIMARY KEY (userID,fileName)
);
create table Files
(
name VARCHAR(20),
fsize INT,
numberofpermissions INT,
constraints PRI_KEY2 PRIMARY KEY (name)
);
create table Users
(
id VARCHAR(20),
password VARCHAR (20),
constraint Pri_key3 PRIMARY KEY (id)
);
-- after all tables create:
alter table Permissions
add constraint Forn_key FOREIGN KEY (userID) REFERENCES Users(id)
INITIALLY DEFERRED DEFERRABLE;
alter table Permissions
add constraint Forn_key2 FOREIGN KEY (filename) REFERENCES Files(name)
INITIALLY DEFERRED DEFERRABLE;
insert into Permissions VALUES ('Agenda','Jones','read','19-JAN-10',30);
insert into Permissions VALUES ('Agenda','Chun','read','19-JAN-10',30);
insert into Permissions VALUES ('Agenda','Rashid','write','17-JAN-10',50);
insert into Permissions VALUES ('Finance','Chun','write','05-DEC-09',50);
insert into Permissions VALUES ('AnnualReport','Jones','write','12-DEC-09',50);
insert into Users VALUES ('Jones', 'duck');
insert into Users VALUES ('Chun', 'tiger');
insert into Users VALUES ('Adams', 'shark');
insert into Users VALUES ('Rashid', 'puma');
insert into Files VALUES ('Agenda', 32, 3);
insert into Files VALUES ('FinanceTables',645, 0);
insert into Files VALUES ('Finance', 120, 1);
insert into Files VALUES ('AnnualReport', 1205, 1);
commit;
I Am now trying to write a SQL command to display for each user who has
permissions for files of a total size of more than 50: the user’s
id, the total size of all the files the user has permissions for, and
the user’s password.
Here is what I have so far but when i try to add anything in to get the password, SQL+ throws up a hissy fit and there will be a hole in my screen soon!
SELECT permissions.userID, sum(fsize) AS Totalsize
FROM files, permissions
where permissions.filename = files.name
group by permissions.userid
having SUM(fsize) > 50;
In oracle, you need to specify the entire group by
SELECT permissions.userID, users.password, sum(fsize) AS Totalsize
FROM files, permissions, users
where permissions.filename = files.name
and users.id = permissions.userID
group by permissions.userid, permissions.password
having SUM(fsize) > 50;
This is different from MySQL where the group by can be implied, but this is more correct.
Join against the user table, and add the password to the group by clause:
SELECT permissions.userID, users.password, sum(fsize) AS Totalsize
FROM files, permissions, users
where permissions.filename = files.name
and users.id = permissions.userID
group by permissions.userid, users.password
having SUM(fsize) > 50;
Use the JOIN syntax. It complies with ANSI SQL. Joining using WHERE is an old syntax that should not be used any more.
SELECT
u.id AS userid, u.password, SUM(f.fsize) AS Totalsize
FROM
users u
INNER JOIN permissions p
ON u.id = p.userID
INNER JOIN files f
ON p.filename = f.name
GROUP BY
u.id, u.password
HAVING
SUM(f.fsize) > 50;
Note that conditions based on aggregation functions must be placed in the HAVING clause. The difference between the WHERE and the HAVING clause is, that WHERE is executed before grouping and HAVING is executed after grouping.
Also, the GROUP BY clause must contain all the expressions from the SELECT-list that don't have an aggregation function.
This should work:
SELECT Users.id, Users.password, Totalsize
FROM Users
INNER JOIN
(SELECT userId, SUM(fSize) AS TotalSize
FROM permissions
INNER JOIN files ON permissions.filename = files.name
group by permissions.userid
having SUM(fsize) > 50) t ON t.userId = Users.Id
This could be done without the inner join of course, however if you gonna need more fields this makes it easier to add them without having to add the extra fields to the group by clause.