add custom authentication checks

This commit is contained in:
Dzmitry_Tamashevich@epam.com
2020-11-04 23:01:08 +03:00
committed by Daniel Hutzel
parent 3cf02cb567
commit 70b0c85346
14 changed files with 172 additions and 112 deletions

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,3 +1,5 @@
using {managed} from '@sap/cds/common';
namespace sap.capire.media.store;
aspect Named {
@@ -17,7 +19,7 @@ aspect Person {
phone : String(24);
fax : String(24);
email : String(60);
password : String(111);
password : String(500);
}
entity MediaTypes : Named {}
@@ -88,7 +90,7 @@ entity InvoiceItems {
quantity : Integer default 1;
}
entity Tracks {
entity Tracks : managed {
key ID : Integer;
name : String(200);
album : Association to Albums;

View File

@@ -7,7 +7,9 @@
"private": true,
"dependencies": {
"@sap/cds": "^4.2.8",
"bcrypt": "^5.0.0",
"express": "^4",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
"passport": "^0.4.1"
},
@@ -17,10 +19,10 @@
"scripts": {
"start": "npx cds run",
"deploy": "cds deploy --to sqlite:mychinook.db",
"rebuild": "npm run deploy && npm run start",
"test": "mocha test/media-service.test.js --verbose --timeout 10000"
},
"cds": {
"ACCESS_TOKEN_SECRET": "secret",
"requires": {
"db": {
"kind": "sqlite",
@@ -30,30 +32,7 @@
}
},
"auth": {
"strategy": "mock",
"users": {
"andrew@chinookcorp.com": {
"password": "some",
"roles": [
"employee"
],
"userAttributes": {
"level": 1,
"ID": 1
}
},
"luisg@embraer.com.br": {
"password": "some",
"roles": [
"customer"
],
"userAttributes": {
"level": 0,
"ID": 1
}
},
"*": true
}
"impl": "srv/auth.js"
}
}
}

View File

@@ -25,9 +25,16 @@ cds.on("bootstrap", (app) => {
});
// add your own middleware before any by cds are added
});
cds.on("served", async ({ db }) => {
cds.on("served", async ({ db, messaging, ...servedServices }) => {
// import data from chinook db if needed
await importData(db);
// add more middleware after all CDS servies
// add logging current user before any request
for (let i in servedServices) {
servedServices[i].prepend((srv) =>
srv.before("*", (req) => console.log("[USER]:", req.user))
);
}
});
// delegate to default server.js:
module.exports = cds.server;

29
media-store/srv/auth.js Normal file
View File

@@ -0,0 +1,29 @@
const cds = require("@sap/cds");
const jwt = require("jsonwebtoken");
const { ACCESS_TOKEN_SECRET } = cds.env;
class MyUser extends cds.User {
constructor(attr, roles, id) {
super({ attr, _roles: roles, id });
}
}
module.exports = (req, res, next) => {
const { authorization: authHeader } = req.headers;
const token = authHeader && authHeader.split(" ")[1];
if (token === null) {
return res.sendStatus(401);
}
try {
const decodedUser = jwt.verify(token, ACCESS_TOKEN_SECRET);
req.user = new MyUser(
{ ID: decodedUser.ID },
decodedUser.roles,
decodedUser.email
);
} catch (error) {
} finally {
next();
}
};

View File

@@ -1,8 +1,8 @@
using {sap.capire.media.store as my} from '../db/schema';
using {BrowseTracks.Tracks} from './browse-tracks-service';
@(requires : 'customer')
service BrowseInvoices {
service BrowseInvoices @(requires : 'customer') {
@readonly
entity Invoices as projection on my.Invoices;
@@ -13,6 +13,10 @@ service BrowseInvoices {
action cancelInvoice(ID : Integer);
/*
Below entities exposed
due to 'navigation property errors' when expanding with odata
*/
@readonly
entity Tracks as projection on my.Tracks excluding {
alreadyOrdered

View File

@@ -16,16 +16,11 @@ module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service
const { Invoices, InvoiceItems } = db.entities;
this.before("*", (req) => {
console.log(
"[USER]:",
req.user.id,
" [LEVEL]: ",
req.user.attr.level,
"[ROLE]",
req.user.is("user") ? "user" : "other"
);
});
// this.before("*", (req) => {
// if (!req.user.is("customer")) {
// req.reject(403);
// }
// });
this.on("READ", "Invoices", async (req) => {
return await db.run(req.query.where({ customer_ID: req.user.attr.ID }));

View File

@@ -2,24 +2,36 @@ using {sap.capire.media.store as my} from '../db/schema';
service BrowseTracks {
@readonly
entity Tracks as projection on my.Tracks excluding {
entity Tracks as projection on my.Tracks excluding {
alreadyOrdered
};
@(requires : 'authenticated-user')
@readonly
entity MarkedTracks as projection on my.Tracks;
entity MarkedTracks @(restrict : [
{
grant : ['*', ],
to : 'customer'
},
{
grant : '*',
to : 'employee'
},
]) as projection on my.Tracks;
/*
Below entities exposed
due to 'navigation property errors' when expanding with odata
*/
@readonly
entity Genres as projection on my.Genres {
entity Genres as projection on my.Genres {
* , tracks : redirected to Tracks
};
@readonly
entity Albums as projection on my.Albums {
entity Albums as projection on my.Albums {
* , tracks : redirected to Tracks
};
@readonly
entity Artists as projection on my.Artists;
entity Artists as projection on my.Artists;
}

View File

@@ -6,26 +6,21 @@ const selectTracksByEmail = (email) => `
join sap_capire_media_store_Invoices invoices
on tracks.ID = invoiceItems.track_ID
join sap_capire_media_store_InvoiceItems invoiceItems
on (invoices.ID = invoiceItems.invoice_ID and invoices.status='2') or
(invoices.ID = invoiceItems.invoice_ID and invoices.status='1')
on invoices.ID = invoiceItems.invoice_ID
join sap_capire_media_store_Customers customers
on customers.ID = invoices.customer_ID
where customers.email='${email}'
where (customers.email='${email}' and invoices.status='2')
or (customers.email='${email}' and invoices.status='1')
`;
module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service
this.before("*", (req) => {
console.log(
"[USER]:",
req.user.id,
" [LEVEL]: ",
req.user.attr.level,
"[ROLE]",
req.user.is("user") ? "user" : "other"
);
});
// this.before("READ", "MarkedTracks", (req) => {
// if (!req.user.is("customer")) {
// req.reject(403);
// }
// });
this.on("READ", "MarkedTracks", async (req) => {
const myTrackIds = (await db.run(selectTracksByEmail(req.user.id))).map(

View File

@@ -1,9 +1,13 @@
using {sap.capire.media.store as my} from '../db/schema';
@(requires : 'employee')
service ManageStore {
entity Tracks as projection on my.Tracks;
action addTrack(name : String(25), albumTitle : String(255), genreName : String(255), composer : String(255));
entity Albums as projection on my.Albums;
entity Genres as projection on my.Genres;
service ManageStore @(requires : 'employee') {
entity Tracks as projection on my.Tracks;
entity Albums as projection on my.Albums;
entity Artists as projection on my.Artists;
/*
Below entities exposed
due to errors when creating Tracks/Albums/Artists
*/
entity MediaTypes as projection on my.MediaTypes;
entity Genres as projection on my.Genres;
}

View File

@@ -3,24 +3,24 @@ const cds = require("@sap/cds");
module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service
const { Genres, Albums } = db.entities;
const { Albums, Tracks, Artists } = db.entities;
this.before("*", (req) => {
console.log(
"[USER]:",
req.user.id,
" [LEVEL]: ",
req.user.attr.level,
"[ROLE]",
req.user.is("user") ? "user" : "other"
this.before("CREATE", "Tracks", async (req) => {
let { ID: lastTrackId } = await db.run(
SELECT.one(Tracks).columns("ID").orderBy({ ID: "desc" })
);
req.data = { ...req.data, ID: ++lastTrackId };
});
this.on("addTrack", async (req) => {
const { albumTitle, genreName, name: trackName, composer } = req.data;
const genre = await db.run(SELECT.one(Genres).where({ name: genreName }));
const album = await db.run(SELECT.one(Albums).where({ title: albumTitle }));
// todo impl
this.before("CREATE", "Artists", async (req) => {
let { ID: lastArtistId } = await db.run(
SELECT.one(Artists).columns("ID").orderBy({ ID: "desc" })
);
req.data = { ...req.data, ID: ++lastArtistId };
});
this.before("CREATE", "Albums", async (req) => {
let { ID: lastAlbumId } = await db.run(
SELECT.one(Albums).columns("ID").orderBy({ ID: "desc" })
);
req.data = { ...req.data, ID: ++lastAlbumId };
});
};

View File

@@ -22,17 +22,34 @@ service Users {
email : String(60);
}
@(requires : 'authenticated-user')
@(restrict : [
{
grant : '*',
to : 'customer'
},
{
grant : '*',
to : 'employee'
},
])
action updatePerson(person : Person);
@(requires : 'authenticated-user')
@(restrict : [
{
grant : '*',
to : 'customer'
},
{
grant : '*',
to : 'employee'
},
])
function getPerson() returns Person;
function mockLogin(email : String(111), password : String(200)) returns {
roles : array of String(111);
level : Integer;
mockedToken : String(500);
email : my.Person.email;
ID : my.Person.ID
action login(email : String(111), password : String(200)) returns {
roles : array of String(111);
token : String(500);
email : String(500);
ID : Integer;
};
}

View File

@@ -1,6 +1,9 @@
const cds = require("@sap/cds");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const USER_LEVELS = { customer: 1, employee: 2 };
const { ACCESS_TOKEN_SECRET } = cds.env;
const ACCESS_TOKEN_EXP_IN = "10m";
module.exports = async function () {
const db = await cds.connect.to("db");
@@ -8,17 +11,6 @@ module.exports = async function () {
const getUserEntity = (isCustomer) => (isCustomer ? Customers : Employees);
this.before("*", (req) => {
console.log(
"[USER]:",
req.user.id,
" [LEVEL]: ",
req.user.attr.level,
"[ROLE]",
req.user.is("user") ? "user" : "other"
);
});
this.on("updatePerson", async (req) => {
await UPDATE(
getUserEntity(req.user && req.user._roles && req.user.is("customer"))
@@ -48,25 +40,37 @@ module.exports = async function () {
);
});
this.on("mockLogin", async (req) => {
this.on("login", async (req) => {
const { email, password } = req.data;
let userFromDb = await db.run(SELECT.one(Employees).where({ email }));
let role = "employee";
let roles = ["employee"];
if (!userFromDb) {
userFromDb = await db.run(SELECT.one(Customers).where({ email }));
role = "customer";
roles = ["customer"];
}
if (!userFromDb || password !== userFromDb.password) {
const userEqualPassword = await bcrypt.compare(
password,
userFromDb.password
);
if (!userEqualPassword) {
req.reject(401);
}
const token = jwt.sign(
{ email, ID: userFromDb.ID, roles },
ACCESS_TOKEN_SECRET,
{
expiresIn: ACCESS_TOKEN_EXP_IN,
}
);
return {
mockedToken: Buffer.from(`${email}:${password}`).toString("base64"),
level: USER_LEVELS[role],
token,
roles,
email: userFromDb.email,
ID: userFromDb.ID,
roles: [role],
};
});
};

View File

@@ -1,4 +1,7 @@
const { resolve } = require("@sap/cds");
const cds = require("@sap/cds");
const bcrypt = require("bcrypt");
const saltRounds = 10;
const FIRST_INDEX = 0;
const ZERO_VALUE = 0;
@@ -74,6 +77,16 @@ async function importData(targetDb) {
return;
}
const hashedPassword = await new Promise((resolve, reject) =>
bcrypt.hash("some", saltRounds, (error, hash) => {
if (error) {
reject(error);
}
resolve(hash);
})
);
console.log("hashedPassword", hashedPassword);
for (index in targetCSNEntities) {
const targetEntityName = targetCSNEntitiesNames[index];
console.log(`[LOG]: Processing ${targetEntityName}`);
@@ -110,7 +123,7 @@ async function importData(targetDb) {
columns.push("password");
srcResultRows = srcResultRows.map((row) => ({
...row,
password: "some",
password: hashedPassword,
}));
}