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; namespace sap.capire.media.store;
aspect Named { aspect Named {
@@ -17,7 +19,7 @@ aspect Person {
phone : String(24); phone : String(24);
fax : String(24); fax : String(24);
email : String(60); email : String(60);
password : String(111); password : String(500);
} }
entity MediaTypes : Named {} entity MediaTypes : Named {}
@@ -88,7 +90,7 @@ entity InvoiceItems {
quantity : Integer default 1; quantity : Integer default 1;
} }
entity Tracks { entity Tracks : managed {
key ID : Integer; key ID : Integer;
name : String(200); name : String(200);
album : Association to Albums; album : Association to Albums;

View File

@@ -7,7 +7,9 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sap/cds": "^4.2.8", "@sap/cds": "^4.2.8",
"bcrypt": "^5.0.0",
"express": "^4", "express": "^4",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1", "moment": "^2.29.1",
"passport": "^0.4.1" "passport": "^0.4.1"
}, },
@@ -17,10 +19,10 @@
"scripts": { "scripts": {
"start": "npx cds run", "start": "npx cds run",
"deploy": "cds deploy --to sqlite:mychinook.db", "deploy": "cds deploy --to sqlite:mychinook.db",
"rebuild": "npm run deploy && npm run start",
"test": "mocha test/media-service.test.js --verbose --timeout 10000" "test": "mocha test/media-service.test.js --verbose --timeout 10000"
}, },
"cds": { "cds": {
"ACCESS_TOKEN_SECRET": "secret",
"requires": { "requires": {
"db": { "db": {
"kind": "sqlite", "kind": "sqlite",
@@ -30,30 +32,7 @@
} }
}, },
"auth": { "auth": {
"strategy": "mock", "impl": "srv/auth.js"
"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
}
} }
} }
} }

View File

@@ -25,9 +25,16 @@ cds.on("bootstrap", (app) => {
}); });
// add your own middleware before any by cds are added // 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); 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; 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 {sap.capire.media.store as my} from '../db/schema';
using {BrowseTracks.Tracks} from './browse-tracks-service'; using {BrowseTracks.Tracks} from './browse-tracks-service';
@(requires : 'customer')
service BrowseInvoices { service BrowseInvoices @(requires : 'customer') {
@readonly @readonly
entity Invoices as projection on my.Invoices; entity Invoices as projection on my.Invoices;
@@ -13,6 +13,10 @@ service BrowseInvoices {
action cancelInvoice(ID : Integer); action cancelInvoice(ID : Integer);
/*
Below entities exposed
due to 'navigation property errors' when expanding with odata
*/
@readonly @readonly
entity Tracks as projection on my.Tracks excluding { entity Tracks as projection on my.Tracks excluding {
alreadyOrdered alreadyOrdered

View File

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

View File

@@ -6,10 +6,22 @@ service BrowseTracks {
alreadyOrdered alreadyOrdered
}; };
@(requires : 'authenticated-user')
@readonly @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 @readonly
entity Genres as projection on my.Genres { entity Genres as projection on my.Genres {
* , tracks : redirected to Tracks * , tracks : redirected to Tracks

View File

@@ -6,26 +6,21 @@ const selectTracksByEmail = (email) => `
join sap_capire_media_store_Invoices invoices join sap_capire_media_store_Invoices invoices
on tracks.ID = invoiceItems.track_ID on tracks.ID = invoiceItems.track_ID
join sap_capire_media_store_InvoiceItems invoiceItems join sap_capire_media_store_InvoiceItems invoiceItems
on (invoices.ID = invoiceItems.invoice_ID and invoices.status='2') or on invoices.ID = invoiceItems.invoice_ID
(invoices.ID = invoiceItems.invoice_ID and invoices.status='1')
join sap_capire_media_store_Customers customers join sap_capire_media_store_Customers customers
on customers.ID = invoices.customer_ID 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 () { module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service const db = await cds.connect.to("db"); // connect to database service
this.before("*", (req) => { // this.before("READ", "MarkedTracks", (req) => {
console.log( // if (!req.user.is("customer")) {
"[USER]:", // req.reject(403);
req.user.id, // }
" [LEVEL]: ", // });
req.user.attr.level,
"[ROLE]",
req.user.is("user") ? "user" : "other"
);
});
this.on("READ", "MarkedTracks", async (req) => { this.on("READ", "MarkedTracks", async (req) => {
const myTrackIds = (await db.run(selectTracksByEmail(req.user.id))).map( 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'; using {sap.capire.media.store as my} from '../db/schema';
@(requires : 'employee') service ManageStore @(requires : 'employee') {
service ManageStore {
entity Tracks as projection on my.Tracks; 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 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; entity Genres as projection on my.Genres;
} }

View File

@@ -3,24 +3,24 @@ const cds = require("@sap/cds");
module.exports = async function () { module.exports = async function () {
const db = await cds.connect.to("db"); // connect to database service 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) => { this.before("CREATE", "Tracks", async (req) => {
console.log( let { ID: lastTrackId } = await db.run(
"[USER]:", SELECT.one(Tracks).columns("ID").orderBy({ ID: "desc" })
req.user.id,
" [LEVEL]: ",
req.user.attr.level,
"[ROLE]",
req.user.is("user") ? "user" : "other"
); );
req.data = { ...req.data, ID: ++lastTrackId };
}); });
this.before("CREATE", "Artists", async (req) => {
this.on("addTrack", async (req) => { let { ID: lastArtistId } = await db.run(
const { albumTitle, genreName, name: trackName, composer } = req.data; SELECT.one(Artists).columns("ID").orderBy({ ID: "desc" })
);
const genre = await db.run(SELECT.one(Genres).where({ name: genreName })); req.data = { ...req.data, ID: ++lastArtistId };
const album = await db.run(SELECT.one(Albums).where({ title: albumTitle })); });
// todo impl 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); email : String(60);
} }
@(requires : 'authenticated-user') @(restrict : [
{
grant : '*',
to : 'customer'
},
{
grant : '*',
to : 'employee'
},
])
action updatePerson(person : Person); action updatePerson(person : Person);
@(requires : 'authenticated-user') @(restrict : [
{
grant : '*',
to : 'customer'
},
{
grant : '*',
to : 'employee'
},
])
function getPerson() returns Person; function getPerson() returns Person;
function mockLogin(email : String(111), password : String(200)) returns { action login(email : String(111), password : String(200)) returns {
roles : array of String(111); roles : array of String(111);
level : Integer; token : String(500);
mockedToken : String(500); email : String(500);
email : my.Person.email; ID : Integer;
ID : my.Person.ID
}; };
} }

View File

@@ -1,6 +1,9 @@
const cds = require("@sap/cds"); 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 () { module.exports = async function () {
const db = await cds.connect.to("db"); const db = await cds.connect.to("db");
@@ -8,17 +11,6 @@ module.exports = async function () {
const getUserEntity = (isCustomer) => (isCustomer ? Customers : Employees); 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) => { this.on("updatePerson", async (req) => {
await UPDATE( await UPDATE(
getUserEntity(req.user && req.user._roles && req.user.is("customer")) 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; const { email, password } = req.data;
let userFromDb = await db.run(SELECT.one(Employees).where({ email })); let userFromDb = await db.run(SELECT.one(Employees).where({ email }));
let role = "employee"; let roles = ["employee"];
if (!userFromDb) { if (!userFromDb) {
userFromDb = await db.run(SELECT.one(Customers).where({ email })); 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); req.reject(401);
} }
const token = jwt.sign(
{ email, ID: userFromDb.ID, roles },
ACCESS_TOKEN_SECRET,
{
expiresIn: ACCESS_TOKEN_EXP_IN,
}
);
return { return {
mockedToken: Buffer.from(`${email}:${password}`).toString("base64"), token,
level: USER_LEVELS[role], roles,
email: userFromDb.email, email: userFromDb.email,
ID: userFromDb.ID, ID: userFromDb.ID,
roles: [role],
}; };
}); });
}; };

View File

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