Skip to content
Merged

Auth #362

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions packages/api/lib/controllers/boxesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ const getBox = async function getBox (req, res, next) {
* @apiParam (RequestBody) {String="hdc1080","bmp280","tsl45315","veml6070","sds011","bme680","smt50","soundlevelmeter", "windspeed"} [sensorTemplates] Specify which sensors should be included.
* @apiParam (RequestBody) {Object} [mqtt] specify parameters of the MQTT integration for external measurement upload. Please see below for the accepted parameters
* @apiParam (RequestBody) {Object} [ttn] specify parameters for the TTN integration for measurement from TheThingsNetwork.org upload. Please see below for the accepted parameters
* @apiParam (RequestBody) {Boolean="true","false"} [useAuth] whether to use access_token or not for authentication
*
* @apiUse LocationBody
* @apiUse SensorBody
Expand Down Expand Up @@ -424,7 +425,8 @@ const getSketch = async function getSketch (req, res, next) {
res.header('Content-Type', 'text/plain; charset=utf-8');
try {
const box = await Box.findBoxById(req._userParams.boxId, { populate: false, lean: false });
res.send(box.getSketch({

const params = {
serialPort: req._userParams.serialPort,
soilDigitalPort: req._userParams.soilDigitalPort,
soundMeterPort: req._userParams.soundMeterPort,
Expand All @@ -434,15 +436,22 @@ const getSketch = async function getSketch (req, res, next) {
devEUI: req._userParams.devEUI,
appEUI: req._userParams.appEUI,
appKey: req._userParams.appKey
}));
};

// pass access token only if useAuth is true and access_token is available
if (box.access_token) {
params.access_token = box.access_token;
}

res.send(box.getSketch(params));
} catch (err) {
handleError(err, next);
}
};

/**
* @api {delete} /boxes/:senseBoxId Mark a senseBox and its measurements for deletion
* @apiDescription This will delete all the measurements of the senseBox. Please not that the deletion isn't happening immediately.
* @apiDescription This will delete all the measurements of the senseBox. Please note that the deletion isn't happening immediately.
* @apiName deleteBox
* @apiGroup Boxes
* @apiUse ContentTypeJSON
Expand Down Expand Up @@ -506,7 +515,9 @@ module.exports = {
{ name: 'ttn', dataType: 'object' },
{ name: 'sensors', dataType: ['object'] },
{ name: 'addons', dataType: 'object' },
{ predef: 'location' }
{ predef: 'location' },
{ name: 'useAuth', allowedValues: ['true', 'false'] },
{ name: 'generate_access_token', allowedValues: ['true', 'false'] }
]),
checkPrivilege,
updateBox
Expand Down Expand Up @@ -537,6 +548,7 @@ module.exports = {
{ name: 'windSpeedPort', dataType: 'String', defaultValue: 'C', allowedValues: ['A', 'B', 'C'] },
{ name: 'mqtt', dataType: 'object' },
{ name: 'ttn', dataType: 'object' },
{ name: 'useAuth', allowedValues: ['true', 'false'] },
{ predef: 'location', required: true }
]),
postNewBox
Expand Down
33 changes: 28 additions & 5 deletions packages/api/lib/controllers/measurementsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const
* @apiName getLatestMeasurementOfSensor
* @apiUse BoxIdParam
* @apiUse SensorIdParam
* @apiParam {Boolean="true","false"} [onlyValue] If set to true only returns the measured value without information about the sensor. Requires a sensorId.
*/
const getLatestMeasurements = async function getLatestMeasurements (req, res, next) {
const { _userParams: params } = req;
Expand All @@ -44,6 +45,14 @@ const getLatestMeasurements = async function getLatestMeasurements (req, res, ne
if (params.sensorId) {
const sensor = box.sensors.find(s => s._id.equals(params.sensorId));
if (sensor) {
if(params.onlyValue){
if(!sensor.lastMeasurement){
res.send(undefined);
return;
}
res.send(sensor.lastMeasurement.value);
return;
}
res.send(sensor);

return;
Expand Down Expand Up @@ -92,15 +101,17 @@ const getData = function getData (req, res, next) {
let stringifier;

// IDEA: add geojson point featurecollection format
if (format === 'csv' || (download === 'true')) {
if (format === 'csv') {
res.header('Content-Type', 'text/csv');
res.header('Content-Disposition', `attachment; filename=${sensorId}.${format}`);
stringifier = csvStringifier(['createdAt', 'value'], delimiter);
} else if (format === 'json') {
res.header('Content-Type', 'application/json; charset=utf-8');
// IDEA: add geojson point featurecollection format
stringifier = jsonstringify({ open: '[', close: ']' }, jsonLocationReplacer);
}
if (download === 'true') {
res.header('Content-Disposition', `attachment; filename=${sensorId}.${format}`);
}

let measurementsStream = Measurement.getMeasurementsStream(req._userParams)
.on('error', function (err) {
Expand Down Expand Up @@ -208,12 +219,17 @@ const getDataMulti = async function getDataMulti (req, res, next) {
* @apiParam (RequestBody) {String} value the measured value of the sensor. Also accepts JSON float numbers.
* @apiParam (RequestBody) {RFC3339Date} [createdAt] the timestamp of the measurement. Should conform to RFC 3339. Is needed when posting with Location Values!
* @apiParam (RequestBody) {Location} [location] the WGS84-coordinates of the measurement.
* @apiHeader {String} access_token Box' unique access_token. Will be used as authorization token if box has auth enabled (e.g. useAuth: true)
*/
const postNewMeasurement = async function postNewMeasurement (req, res, next) {
const { boxId, sensorId, value, createdAt, location } = req._userParams;

try {
const box = await Box.findBoxById(boxId, { populate: false, lean: false });
if (box.useAuth && box.access_token && box.access_token !== req.headers.authorization) {
throw new UnauthorizedError('Box access token not valid!');
}

const [measurement] = await Measurement.decodeMeasurements([{
sensor_id: sensorId,
value,
Expand Down Expand Up @@ -261,6 +277,7 @@ const postNewMeasurement = async function postNewMeasurement (req, res, next) {
* @apiUse LocationBody
* @apiParam {String} [luftdaten] Specify whatever you want (like `luftdaten=1`. Signals the api to treat the incoming data as luftdaten.info formatted json.
* * @apiParam {String} [hackair] Specify whatever you want (like `hackair=1`. Signals the api to treat the incoming data as hackair formatted json.
* @apiHeader {String} access_token Box' unique access_token. Will be used as authorization token if box has auth enabled (e.g. useAuth: true)
* @apiParamExample {application/json} JSON-Object:
* {
* "sensorID": "value",
Expand Down Expand Up @@ -317,10 +334,15 @@ const postNewMeasurements = async function postNewMeasurements (req, res, next)

if (Measurement.hasDecoder(contentType)) {
try {
const box = await Box.findBoxById(boxId, { populate: false, lean: false, projection: { sensors: 1, locations: 1, lastMeasurementAt: 1, currentLocation: 1, model: 1, access_token: 1 } });
const box = await Box.findBoxById(boxId, { populate: false, lean: false, projection: { sensors: 1, locations: 1, lastMeasurementAt: 1, currentLocation: 1, model: 1, access_token: 1, useAuth: 1 } });

// if (contentType === 'hackair' && box.access_token !== req.headers.authorization) {
// throw new UnauthorizedError('Box access token not valid!');
// }

if (contentType === 'hackair' && box.access_token !== req.headers.authorization) {
throw new UnauthorizedError('Access token not valid!');
// authorization for all boxes that have not opt out
if ((box.useAuth || contentType === 'hackair') && box.access_token && box.access_token !== req.headers.authorization) {
throw new UnauthorizedError('Box access token not valid!');
}

const measurements = await Measurement.decodeMeasurements(req.body, { contentType, sensors: box.sensors });
Expand Down Expand Up @@ -389,6 +411,7 @@ module.exports = {
retrieveParameters([
{ predef: 'boxId', required: true },
{ predef: 'sensorId' },
{ name: 'onlyValue', required: false }
]),
getLatestMeasurements
]
Expand Down
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"Norwin Roosen"
],
"dependencies": {
"@sensebox/opensensemap-api-models": "^0.0.25",
"@sensebox/opensensemap-api-models": "^0.0.26-beta.0",
"@turf/area": "^6.0.1",
"@turf/bbox": "^6.0.1",
"@turf/centroid": "^6.0.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/models/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

## v0.0.26-beta.0
- Authorization

## v0.0.25
- Add Cayenne LPP Decoder

Expand Down
4 changes: 2 additions & 2 deletions packages/models/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@sensebox/opensensemap-api-models",
"description": "openSenseMap data models and database connection",
"version": "0.0.25",
"version": "0.0.26-beta.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@sensebox/osem-protos": "^1.1.0",
"@sensebox/sketch-templater": "^1.8.2",
"@sensebox/sketch-templater": "^1.8.3-beta.0",
"bcrypt": "^5.0.0",
"bunyan": "^1.8.14",
"config": "^3.3.1",
Expand Down
36 changes: 31 additions & 5 deletions packages/models/src/box/box.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ const boxSchema = new Schema({
},
access_token: {
type: String,
required: false
required: true
},
useAuth: {
type: Boolean,
required: true,
default: false,
}
}, { usePushEach: true });
boxSchema.plugin(timestamp);
Expand Down Expand Up @@ -167,6 +172,7 @@ boxSchema.set('toJSON', {
if (options && options.includeSecrets) {
box.integrations = ret.integrations;
box.access_token = ret.access_token;
box.useAuth = ret.useAuth;
}

return box;
Expand Down Expand Up @@ -225,7 +231,8 @@ boxSchema.statics.initNew = function ({
} = { enabled: false },
ttn: {
app_id, dev_id, port, profile, decodeOptions: ttnDecodeOptions
} = {}
} = {},
useAuth
}) {
// if model is not empty, get sensor definitions from products
// otherwise, sensors should not be empty
Expand All @@ -244,6 +251,14 @@ boxSchema.statics.initNew = function ({
sensors = sensorLayouts.getSensorsForModel(model);
}
}
if(model){
//activate useAuth only for certain models until all sketches are updated
if(['homeV2Lora' , 'homeV2Ethernet' , 'homeV2EthernetFeinstaub' , 'homeV2Wifi' , 'homeV2WifiFeinstaub' , 'homeEthernet' , 'homeWifi' , 'homeEthernetFeinstaub' , 'homeWifiFeinstaub' , 'hackair_home_v2'].indexOf(model) != -1){
useAuth = true
} else {
useAuth = false
}
}

const integrations = {
mqtt: { enabled, url, topic, decodeOptions: mqttDecodeOptions, connectionOptions, messageFormat },
Expand Down Expand Up @@ -271,7 +286,8 @@ boxSchema.statics.initNew = function ({
model,
sensors,
integrations,
access_token
access_token,
useAuth
});

};
Expand Down Expand Up @@ -801,7 +817,7 @@ boxSchema.methods.updateSensors = function updateSensors (sensors) {
}
};

boxSchema.methods.getSketch = function getSketch ({ encoding, serialPort, soilDigitalPort, soundMeterPort, windSpeedPort, ssid, password, devEUI, appEUI, appKey } = {}) {
boxSchema.methods.getSketch = function getSketch ({ encoding, serialPort, soilDigitalPort, soundMeterPort, windSpeedPort, ssid, password, devEUI, appEUI, appKey, access_token } = {}) {
if (serialPort) {
this.serialPort = serialPort;
}
Expand All @@ -820,6 +836,7 @@ boxSchema.methods.getSketch = function getSketch ({ encoding, serialPort, soilDi
this.devEUI = devEUI;
this.appEUI = appEUI,
this.appKey = appKey;
this.access_token = access_token;

return templateSketcher.generateSketch(this, { encoding });
};
Expand Down Expand Up @@ -860,12 +877,21 @@ boxSchema.methods.updateBox = function updateBox (args) {
const box = this;

// only grouptag, description and weblink can removed through setting them to empty string ('')
for (const prop of ['name', 'exposure', 'grouptag', 'description', 'weblink', 'image', 'integrations.mqtt', 'integrations.ttn', 'model']) {
for (const prop of ['name', 'exposure', 'grouptag', 'description', 'weblink', 'image', 'integrations.mqtt', 'integrations.ttn', 'model', 'useAuth']) {
if (typeof args[prop] !== 'undefined') {
box.set(prop, (args[prop] === '' ? undefined : args[prop]));
}
}

// if user wants a new access_token
if (typeof args['generate_access_token'] !== 'undefined') {
if (args['generate_access_token'] == 'true') {
// Create new acces token for box
const access_token = crypto.randomBytes(32).toString('hex');
box.set('access_token', access_token);
}
}

if (sensors) {
box.updateSensors(sensors);
} else if (addonToAdd) {
Expand Down
32 changes: 19 additions & 13 deletions packages/models/src/user/mails.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,30 @@ if (config.get('mailer.url')) {

const mailTemplates = {
'newBox' (user, box) {
let sketchParams = {
encoding: 'base64',
ssid: '',
password: '',
serialPort: box.serialPort,
soilDigitalPort: box.soilDigitalPort,
soundMeterPort: box.soundMeterPort,
windSpeedPort: box.windSpeedPort,
devEUI: '',
appEUI: '',
appKey: ''
}

if(box.access_token) {
sketchParams.access_token = box.access_token
}

return {
payload: {
box
},
attachment: {
filename: 'senseBox.ino',
contents: box.getSketch({
encoding: 'base64',
ssid: '',
password: '',
serialPort: box.serialPort,
soilDigitalPort: box.soilDigitalPort,
soundMeterPort: box.soundMeterPort,
windSpeedPort: box.windSpeedPort,
devEUI: '',
appEUI: '',
appKey: ''
})
contents: box.getSketch(sketchParams)
}
};
},
Expand Down Expand Up @@ -112,7 +118,7 @@ if (config.get('mailer.url')) {
},
attachment: {
filename: 'senseBox.ino',
contents: box.getSketch({ encoding: 'base64' })
contents: box.getSketch({ encoding: 'base64', access_token: box.access_token })
}
};
},
Expand Down
6 changes: 6 additions & 0 deletions tests/data/getUserBoxesSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ module.exports = {
'updatedAt': {
'type': 'string'
},
'useAuth': {
'type': 'boolean'
},
'access_token': {
'type': 'string'
},
'currentLocation': {
'type': 'object',
'properties': {
Expand Down
10 changes: 9 additions & 1 deletion tests/data/senseBoxSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ module.exports = {
'image': {
'type': 'string'
},
'useAuth': {
'type': "boolean"
},
'access_token': {
'type': "string"
},
'sensors': {
'type': 'array',
'items': {
Expand Down Expand Up @@ -115,6 +121,8 @@ module.exports = {
'exposure',
'sensors',
'currentLocation',
'loc'
'loc',
// 'useAuth',
// 'access_token'
]
};
1 change: 0 additions & 1 deletion tests/data/valid_sensebox.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ module.exports = function ({ bbox, model, sensors, lonlat, name = '', sensorTemp
'connectionOptions': ''
}
};

if (sensors) {
box.sensors = sensors;
}
Expand Down
Loading