Giter Club home page Giter Club logo

maprules's Introduction

MapRules

Custom mapping presets and validation rules

About

MapRules is an api service that allows mappers and mapping campaign managers to define custom mapping presets and validation rules usable in OpenStreetMap Editors.

The goal of MapRules is to simplify OpenStreetMap feature tagging and validation.

Documentation

Contributing

Links to other MapRules Repos

...see the Architecture for a technical description of the repos work together

Getting Started

Install Dependencies

Install sqlite

ubuntu!

sudo apt-get update
sudo apt-get install -yq sqlite3 libsqlite-dev

centos!

sudo yum update
sudo yum install -yq

mac!

brew install sqlite3

windows!

...use this for guidance!

use node 10.x

# with nvm installed and from root of MapRules directory...
nvm install #only run first time if you don't have the right version
nvm use

...see here for setting up nvm on a linux machine ...see here for setting up nvm on a windows machine

install node dependencies
yarn install -G sqlite3 && yarn install

Development

Get Consumer Token and Consumer Secrete Keys from a development instance of OSM Site

A "development instance" of the OSM Site could be the openstreetmap-website that you clone and run on your machine, or (perhaps easier) you could be one of the development instances that OSM provides like https://master.apis.dev.openstreetmap.org

To actually get the tokens, once you have a login for your development instance, go to the /user/username/oauth_clients/new web page and fill out the form. When it comes to permissions, only select the read their user preferences checkbox. Use Consumer Token and Consumer Secret keys provided once you submit the form for the CONSUMER_KEY and CONSUMER_SECRET environmental variables. So too should you supply the development osm site you use as the OSM_SITE environmental variable.

build docs and icons lookup tables

...the iD editor's presets use very nice icons. the ICON lookup table tries to match the custom maprules presets with icons made for presets with matching tags

yarn build

Migrate the db and seed it with fixture data

NODE_ENV=development JWT=${some.jwt} yarn fixture

..note, the JWT value above needs to be used whenever running the app in the same NODE_ENV.

Spin up the server

PORT=3001 yarn dev

Test

yarn test // propended with all needed env variables...

test with docker image

docker build -f Dockerfile . && docker run MapRules /bin/bash -c 'npm run test:fixture // with needed environmental variables'

Configure process.yml

MapRules uses PM2 command line tool to manage the service when running in production. If PM2 is for you, its certainly a great option for local development.

The process.yml file acts as the configuration file for PM2, and since it holds all the kinds of secret keys, you never want to commit this to a remote branch/make it exposed outside the machine you use to run maprules.

As such, and this might be overkill that can be changed in the future, the process.yml is built from a process.yml.in, which we keep gitignored.

So, create a process.yml.in file and place a configuration like the one below in it, using the client keys, osm site, and secret session/jwt keys for maprules. Note, we also give a fully hydrated example for the classic development, staging, production environment set used for software in a process.yml.example file.

apps:
   - name: maprules
   script: index.js
   env_production
      NODE_ENV: production
      PORT: ${YOUR.FAVORITE.PORT}
      HOST: ${YOUR.FAVORITE.HOST}
      CONSUMER_KEY: ${CONSUMER.KEY.FROM.OSM.SITE},
      CONSUMER_SECRET: ${CONSUMER.SECRET.FROM.OSM.SITE},
      OSM_SITE: ${URL.TO.OSM}
      YAR: ${PRIVATE.KEY.FOR.YAR},
      JWT: ${PRIVATE.KEY.FOR.JWT}

With everything in your process.yml.in file, run the build script again and you'll be ready to use PM2 as you need. The prod npm command is an example of how to get maprules running with pm2 for a configuration that matches the yml snippet above.

maprules's People

Contributors

abalosc1 avatar maxgrossman avatar nlehuby avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

nlehuby pyrog

maprules's Issues

move all handler functions to the routes files.

I think I went a bit modular crazy when I first wrote the handlers and routes because right now I'm in this file inception that's making it hard to keep my focus on code/tasks...

let's move all the handler functions over to the routes files so they are all in one place!!!

fix josm preset adapter

...looks like the josm preset isn't considering the text input cases!!
this ticket will fix that

User database migrations

We need 2 db migrations for user authentication

One for adding user table. So maybe...

CREATE TABLE users (
  'id' integer not null primary key,
  'name' text not null
)

Another for sessions.

CREATE TABLE user_sessions (
  'id' uuid not null primary key,
  'user_id' integer not null,
  'user_agent' text not null,
  'created_at' date not null
)
CONSTRAINT fk_users
  FOREIGN KEY (user_id)
  REFERENCES users(id);

Another for adding user keys columns to presets table. So schema is now something like...

... (
 'id' uuid not null primary key,
 'user_id' integer not null
 'preset' JSON1

CONSTRAINT fk_users
  FOREIGN KEY (user_id)
  REFERENCES users(id)
)

ref #51

add logout route

We need an auth/logout route so that clients can log out!

It should...

  1. take the JWT in the authorization header and remove the matching user_sessions table record

Make sure this pr also has a test!

enforce correct and add missing http error responses

As the users feature inches ever closer to being ready to be put in dev, there's one more chore I think.

Just need to do a sanity check throughout routes to make sure that the errors we are providing to the client are correct/meaningful/distinguishable so that our user interface can correctly handle deleting their tokens, show the user the logged out state, etc...

Scenarios I imagine are

  • the token is a valid JWT but either not known to service or out of date: provide 401
  • the token is 'malformed', either not a real JWT or it does not match our JWT schema, 400 error
  • the token is valid and we know about it, a 200 ok response

update callback route with query param validation....

one thing missing from the callback route's handler is a check to make sure that both the oauth_token and oauth_verify tokens are provided in a request's query parameters.

Right now, the route just sort of assumes those params are there, meaningful and goes about trying to map them to a current session.

Looking at the hapi documentation, it might make sense combine both the query param validation and session look up in a pre method

In that pre-method, the checks would be to...

  1. see that both the oauth_token and oauth_verify are present (not sure what other validating is needed)
  2. check that one of the current sessions in the sessionManager is in our yar lookup table (we currently do this here right in the handler)
  3. return the matching oauth_token and the query's ouath verifier from the pre-method as an object like { ouath_token: ..., oauth_verifier: ... }

In each of these steps, if the query params aren't there, or matching session is not found, then throw an error (maybe 401 unauthorized? here I am not sure)...

Making this move would let the handler function just have what it needs such that

handler: (r,h) {
  const { oauth_token, oauth_verifier } = r.pre.callbackValidation
 /// go about business 
}

Is this good? bad? make sense?

OSM OAuth/Users Epic

Before we take steps to integrate the application with other osm services (like the hot tm), we ought to create users so that only logged in OSM users can create/update/delete MapRules...

Following documentation from the OSM site and other popular apps (like TM), the way to do this is to make MapRules an OAuth client with the OSM OAuth mechanism. MapRules will use the OSM OAuth to create user records that we save in the MapRules database. These records will include a user id and session token, both of which rely on authenticating with the OSM OAuth first.

Below are the 2 main components to making this happen.

Logging into MapRules

1a. get our client template, particularly for the consumer_key and consumer_secret codes, from here

  1. A client, the maprules-ui web app, makes a request to a new endpoint called /auth/login
  • that endpoint's handler will make a request to the OSM site's request_token endpoint, and include the MapRules /auth/callback endpoint, the MapRules' consumer_key code, and the consumer_secret code.
  • /request_token responds back the oauth_token and oauth_token_secret codes.
  • we keep a record of those codes in our request's 'session', then send a request to the /oauth/authorize?${oauth_token} endpoint. Doing so opens up a popup for the user to log into OpenStreetMap...
  • once logged into to OpenStreetMap, the OpenStreetMap service will tell the client to make a request to the callback url we specified when we made a request to the /request_token endpoint...
  1. The auth/callback endpoint receives a request that includes 2 codes as query parameters, oauth_token and oauth_verifier
  • This endpoint handler makes a request to OSM's /access_token endpoint, providing the oauth_token, ouath_token_secret, and oauth_verifier, consumer_secret, and consumer_key codes...
  • This request provides back access_token and access_token_secret codes...
  1. The MapRules service makes a request to the user/details endpoint with the access_token and access_token secret codes we retrieved in step 2
  • this returns an xml document with information, including user name and id about the user...
  1. we take user details to create/update a user record in the database.
  • if no user record exists, make a new one with user_id from user details as the primary key and a session hash (maybe generated with something like the uuid package we have already in the service), as well as a timestamp (to use for expiring tokens...)
  • if a user does exist in the table, see if the session is out of date, and if so, create a new token and timestamp
  1. we take the current user record to generate a JSON Web Token
  • the decoded token will be...
{
   id: ${user.id},
   username: ${user.name},
   session: ${session.id}
}
  • looks like this is a popular package for decoding/encoding the token
  1. encode the JWT and reply it back to the client to be saved in the browser and used for future CRUD requests...

CRUD Requests in MapRules

Endpoints that allow users to do POST/PUT/DELETE operations need to do the following beforehand

  1. see if a session JWT is provided in the request
    - if not, throw a 401 unauthorized...
  2. (for PUT/DELETE) see if JWT is valid for the resource of interest
    - if JWT is not for resource's owner, throw a 403
    - if JWT is valid for resource's owner, but session id is not the one in the user table, throw a 401
  • the client should handle the 401 cases by making the user try to log back in
  • the client should kick/scream/make it clear the user is trying to do something very uncool if a 403...

Phew, that was a lot of pseudo code and thinking!!! As for the next steps...

  • Setup MapRules as an OAuth client with OpenStreetMap OAuth for the Logging into MapRules steps...
  • Implement the needed /auth/${login/callback} endpoints needed for logging into MapRules....
  • Create database migrations that will include new user table and update preset schema so presets table has a user so we can enforce ownership
  • introduce JWT library for creating, decoding JWTs
  • update all CRUD endpoints so they enforce the steps mentioned above...

I'll make tickets for these!

handle the last leg of oAuth

#52 still has one more leg of work left.

I had not really thought about what we do when the callback URL is successful.
We cannot do what we do now (just return a JWT).

Since the request made to the callback endpoint is a popup window separate from the SPA app that really needs the JWT, we'll need to add "one last leg" to get the JWT to our front end SPA.

To do this, we need the callback URL to return a 302/307 http response with 2 characteristics

  1. the location header set to ${maprules.base}/authorized?user=${user.that.was.logging.in}
  2. an httponly session cookie that maps to one of the login sessions we keep track of in the sessionManager class.

To encrypt this cookie, maybe we just send it through the jwt sign function??

Then, when the front end loads its location to that new path /authorized it will try to make a req to something like /auth/token which will reconcile if http session cookie (which it sends) and the user in the query parameter to a current session in the SessionManager. If it successfully finds one, we reply to the JWT that the app can use for user-protected routes

This def feels convoluted, but the initial decision to use JWT (which has a convention of being sent in an auth header, not some httponly session cookie) leaves us here, needing a final step.

This also implies getting away from yar. I guess I never read docs correctly, but the yar object is handler request specific, it is not some global to server object that you can look up things with. I think if we encrypt session cookies the front end receives we are left at a similar place I was trying to get with yar - don't just make the all the session details readable without some sort of key.

make initial iD preset match scores super high?

In exploration of doing a preset merge and not an overwrite in the iD work, I've found that without setting the external preset matchScores very high, iD does not find them.

For example, if I have a 'Surf Shop' presets with tags {amenity:'shop','shop:type':'surf'}, without overloading the matchScore, iD finds the base amenity preset.

documentation updates!

#31 relies on running the build command before starting the app. Need to add a note in the documentation about this...

handle area tags

In the switch to making point, line, and area the default geometries, there needs to be logic to add an area=yes tag to presets that don't already have tags that infer are (like building=yes).

We have the is-area-keys package as a dependency already, as I was using this when geometries were flipped before #16...

I guess what we can do is check right before we make each area preset and ask..

"hey are there area tags on this preset?"

if there aren't, add the area=yes.

better handle token expiration

JWT has this expiration claim that we can use to handle if a user session is out of date. It tells us the time at which a JWT will expire.

So, any request that requires the token should ask if the token is expired, and if it is, disallow doing the said operation.

I also am thinking that we should have some logic that tries to figure out expiration when a user, with a token, opens up the app in their browser. The use case is, the person opens up the app, they go about updating a MapRules, they then try to click save or something, then we (given the above requirement) just says "you are not authorized to do this".

Better I think would be if when the person opens' the app with a token, the app first makes a request to something like auth/session which will go through a set of checks to see if the session is valid per our jwt strategy (this includes to see if the session has expired).

Requirements

  1. make the jwtScheme check if the token is out of date using the expiration claim. Right now it uses the database's created_at column.
  2. Add auth/session route

set up circleci

Need to add a .circleci folder & the right yml to guide tests. we originally thought about using travis, but why not circle instead!

testing, testing, one two!

do not include default presets for iD.

If we're moving towards merging default and new presets, i think we'll end up making duplicates if the external presets we send to iD includes what it will already have out of the box (like the point, line, area, relation, and vertex presets)...

use preset specific icons for iD presets...

Currently the iD presets we generate just use a maki-natural icon.
This ticket tracks work to make the icons specific to the presets.
For example, a 'health clinic' preset should use the same maki icon that the default clinic preset found in iD uses...

handle rule-less presets

We hadn't considered/tested the scenario where no fields are added to a config such that no rules are neccessary.
Currently the rules endpoint handlers expect rules always.
In these 'no rules' scenarios the rule parser (for iD rules) ends up breaking and leading to a 500 error.
Simple fix is just to return an empty in these cases.

make sure presets are unique.

I made a bad update and whacked code I had to make sure each iD presets config was unique and did not cumulatively add new category members!!!

This adds back that uniqueness

additional eslint rules

let's add a no-unused-imports rule if for nothing else to clean up all the unused imports I polluted the code with ๐Ÿฆ

add route for users

a list of users as it relates to #68 would also be useful. being able to query for only 'active' users, those who've made maprules presets too would be helpful.

support relations

...not sure why we didn't initially try to support relations, but I think it'd be a relatively small lift and we should!

add tests for login/callback route

We need to create tests that make sure we have a handle on the logic in the login and callback routes' handler functions.

Since both make requests out to the osm site, we'll need to stub those requests. I have found nock to be really easy to use for this sort of task, but if there is preference elsewhere, all good.

What I am thinking for the actual tests...

login

  1. make sure code throws an error if a JWT secret key isn't available as this line suggests it should.

  2. make sure the route returns the authorize URL with the oauth_token query parameter we make up in as part of the stub. This implies that all of the bullet points here have occurred for the login route's set of needed steps.

callback

  1. make sure the new pre-function mentioned in #57 works as we expect
    2. see that the code in this else block, which handles how to deal with an already existing user that is trying to login.
  2. Each time an existing user logs in again, the user/user_agent record should be updated if it exists, otherwise, create one for the combo.

Looking at my code and searching about JWT expiration, I'm not certain this else block is really working how it should. So I think for case 2 we need to read up, maybe some gists like this one and or the actual spec to figure out how best to handle this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.