Skip to content

Commit

Permalink
Merge pull request #539 from mapswipe/dev
Browse files Browse the repository at this point in the history
Release OSM Login Functions to production
  • Loading branch information
Hagellach37 committed Jun 9, 2022
2 parents af3e498 + 9bc6d02 commit 061282a
Show file tree
Hide file tree
Showing 24 changed files with 3,144 additions and 30 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ jobs:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
FIREBASE_DB: ${{ secrets.FIREBASE_DB }}
run: |
# Create a mock file for wal-g setup
docker-compose up --build firebase_deploy
docker-compose run firebase_deploy sh -c "firebase use $FIREBASE_DB && firebase deploy --token $FIREBASE_TOKEN --only database:rules"
- name: Decrypt Service Account Key File
working-directory: ./
run: |
Expand Down
19 changes: 16 additions & 3 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ services:
environment:
FIREBASE_TOKEN: '${FIREBASE_TOKEN}'
FIREBASE_DB: '${FIREBASE_DB}'
command: sh -c "firebase use $FIREBASE_DB && firebase deploy --token $FIREBASE_TOKEN --only functions,database:rules"
volumes:
- ./firebase:/firebase
FIREBASE_AUTH_SITE: '${FIREBASE_AUTH_SITE}'
OSM_OAUTH_REDIRECT_URI: '${OSM_OAUTH_REDIRECT_URI}'
OSM_OAUTH_APP_LOGIN_LINK: '${OSM_OAUTH_APP_LOGIN_LINK}'
OSM_OAUTH_API_URL: '${OSM_OAUTH_API_URL}'
OSM_OAUTH_CLIENT_ID: '${OSM_OAUTH_CLIENT_ID}'
OSM_OAUTH_CLIENT_SECRET: '${OSM_OAUTH_CLIENT_SECRET}'
command: >-
sh -c "firebase use $FIREBASE_DB &&
firebase target:apply hosting auth \"$FIREBASE_AUTH_SITE\" &&
firebase functions:config:set
osm.redirect_uri=\"$OSM_OAUTH_REDIRECT_URI\"
osm.app_login_link=\"$OSM_OAUTH_APP_LOGIN_LINK\"
osm.api_url=\"$OSM_OAUTH_API_URL\"
osm.client_id=\"$OSM_OAUTH_CLIENT_ID\"
osm.client_secret=\"$OSM_OAUTH_CLIENT_SECRET\" &&
firebase deploy --token $FIREBASE_TOKEN --only functions,hosting,database:rules"
3 changes: 3 additions & 0 deletions docs/source/dev_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ On how to setup the development environment and how to deploy functions to the F
For more information refer to the official [Reference on Cloud Function for Firebase](https://firebase.google.com/docs/reference/functions/).
For example function take a look at this [GitHub repository](https://github.com/firebase/functions-samples).

### OSM OAuth 2
Firebase functions are also used to allow users to login to MapSwipe with their OpenStreetMap account. Refer to [the notes in the app repository](https://github.com/mapswipe/mapswipe/blob/master/docs/osm_login.md) for more information.


## Database Backup

Expand Down
8 changes: 8 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FIREBASE_DB=
FIREBASE_API_KEY=
FIREBASE_TOKEN=
GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/mapswipe_workers/serviceAccountKey.json"
FIREBASE_AUTH_SITE=

# postgres configuration
POSTGRES_USER=mapswipe_workers
Expand Down Expand Up @@ -31,3 +32,10 @@ SENTRY_DSN=

# osmcha configuration
OSMCHA_API_KEY=

# OSM OAuth Configuration
OSM_OAUTH_REDIRECT_URI=
OSM_OAUTH_API_URL=
OSM_OAUTH_CLIENT_ID=
OSM_OAUTH_CLIENT_SECRET=
OSM_APP_LOGIN_LINK=
11 changes: 10 additions & 1 deletion firebase/.firebaserc
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"projects": {
"default": "dev-mapswipe"
},
"targets": {
"dev-mapswipe": {
"hosting": {
"auth": [
"dev-auth-mapswipe"
]
}
}
}
}
}
4 changes: 4 additions & 0 deletions firebase/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
trailingComma: "all"
tabWidth: 4
singleQuote: true
arrowParens: "avoid"
4 changes: 3 additions & 1 deletion firebase/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
FROM node:12-alpine
FROM node:14-alpine
RUN npm install firebase-functions@latest firebase-admin@latest --save
RUN npm install -g firebase-tools
COPY . /firebase
RUN cd firebase/functions && npm install
WORKDIR /firebase/
49 changes: 47 additions & 2 deletions firebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,51 @@ Then run the container interactively and open a bash shell.
Now you are inside the docker container and can login to firebase. You need to insert an authorization code into the terminal during that process.
* `firebase login --no-localhost`

Finally you can deploy your changes for cloud functions and database rules individually.
* `firebase deploy --only functions`
Finally you can deploy your changes for cloud functions and database rules individually. Hosting must be done as well to
expose the authentication functions publicly.
* `firebase deploy --only functions,hosting`
* `firebase deploy --only database:rules`

## Notes on OAuth (OSM login)

Refer to [the notes in the app repository](https://github.com/mapswipe/mapswipe/blob/master/docs/osm_login.md).

Some specifics about the related functions:
- get a service-account.json file from firebase which allows the OAuth functions to access the database and call
external URLs (this last point only works on a firebase Blaze plan)
- Before deploying, set the required firebase config values in environment:
FIXME: replace env vars with config value names
- OSM_OAUTH_REDIRECT_URI `osm.redirect_uri`: `https://dev-auth.mapswipe.org/token` or `https://auth.mapswipe.org/token`
- OSM_OAUTH_APP_LOGIN_LINK `osm.app_login_link`: 'devmapswipe://login/osm' or 'mapswipe://login/osm'
- OSM_OAUTH_API_URL `osm.api_url`: 'https://master.apis.dev.openstreetmap.org/' or 'https://www.openstreetmap.org/' (include the
trailing slash)
- OSM_OAUTH_CLIENT_ID `osm.client_id`: find it on the OSM application page
- OSM_OAUTH_CLIENT_SECRET `osm.client_secret`: same as above. Note that this can only be seen once when the application is created. Do not
lose it!
- Deploy the functions as explained above
- Expose the functions publicly through firebase hosting, this is done in `/firebase/firebase.json` under the `hosting`
key.

The functions must be publicly exposed to allow anyone to run them without authentication, after they have first been
deployed:
- in firebase console, open the [list of cloud
functions](https://console.cloud.google.com/functions/list?project=dev-mapswipe&authuser=0&hl=en&tab=permissions)
- "allow unauthenticated" is not visible in the "authentication" column, then
- select the auth functions by checking the box to the left side of them in the list
- click "permissions" near the top, then "Add principal"
- under "new principal" pick "allUsers"
- under "select a role, choose "Cloud Function Invoker" and save.
- Confirm all the warnings

See https://firebase.google.com/docs/functions/http-events#invoke_an_http_function for the full story (and
https://cloud.google.com/functions/docs/securing/managing-access-iam#allowing_unauthenticated_http_function_invocation).
If you don't do this, you will get an HTTP 403 error saying you don't have permission to access the function.

You also need to enable the "IAM service account credentials API" by going to
https://console.cloud.google.com/apis/api/iamcredentials.googleapis.com/credentials?project=dev-mapswipe.

Finally, you need to figure out the service account used by the cloud functions (it apparently is `PROJECT_NAME@appspot.gserviceaccount.com` by default) and grant it the right to sign blobs, see https://firebase.google.com/docs/auth/admin/create-custom-tokens#service_account_does_not_have_required_permissions.

We store the user's OSM access token in the database, which right now does not do anything, but would be needed if we
want our backend to do something in OSM on behalf of the user. The database access rules are set to only allow the owner
of a token to access them.
6 changes: 6 additions & 0 deletions firebase/database.rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@
"created",
"teamId"
]
},
"OSMAccessToken": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid"
}
}
},
// leaving this here, since version before v2 pull data from there
Expand Down
14 changes: 14 additions & 0 deletions firebase/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,19 @@
},
"database": {
"rules": "database.rules.json"
},
"hosting": {
"target": "auth",
"public": "public",
"rewrites": [
{
"source": "/redirect",
"function": "osmAuth-redirect"
},
{
"source": "/token",
"function": "osmAuth-token"
}
]
}
}
41 changes: 41 additions & 0 deletions firebase/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()

// all functions are bundled together. It's less than ideal, but it does not
// seem possible to split them using the split system for multiple sites from
// https://firebase.google.com/docs/hosting/multisites
const osmAuthFuncs = require('./osm_auth')

exports.osmAuth = {}

// expose HTTP expossed functions here so that we can pass the admin object
// to them and only instantiate/initialize it once
exports.osmAuth.redirect = functions.https.onRequest((req, res) => {
osmAuthFuncs.redirect(req, res);
});

exports.osmAuth.token = functions.https.onRequest((req, res) => {
osmAuthFuncs.token(req, res, admin);
});

/*
Log the userIds of all users who finished a group to /v2/userGroups/{projectId}/{groupId}/.
Expand All @@ -28,6 +44,7 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro
const totalGroupContributionCountRef = userRef.child('groupContributionCount')
const userContributionRef = userRef.child('contributions/' + context.params.projectId)
const taskContributionCountRef = userRef.child('contributions/' + context.params.projectId + '/taskContributionCount')
const thisResultRef = admin.database().ref('/v2/results/' + context.params.projectId + '/' + context.params.groupId + '/' + context.params.userId )

// if result ref does not contain all required attributes we don't updated counters
// e.g. due to some error when uploading from client
Expand All @@ -45,6 +62,30 @@ exports.groupUsersCounter = functions.database.ref('/v2/results/{projectId}/{gro
return null
}

// Check for specific user ids which have been identified as problematic.
// These users have repeatedly uploaded harmful results.
// Add new user ids to this list if needed.
const userIds = []
if ( userIds.includes(context.params.userId) ) {
console.log('suspicious user: ' + context.params.userId)
console.log('will remove this result and not update counters')
return Promise.all([thisResultRef.remove()])
}

// check if these results are likely to be vandalism
// mapping speed is defined by the average time needed per task in seconds
const numberOfTasks = Object.keys( result['results'] ).length
const startTime = Date.parse(result['startTime']) / 1000
const endTime = Date.parse(result['endTime']) / 1000
const mappingSpeed = (endTime - startTime) / numberOfTasks

if (mappingSpeed < 0.125) {
// this about 8-times faster than the average time needed per task
console.log('unlikely high mapping speed: ' + mappingSpeed)
console.log('will remove this result and not update counters')
return Promise.all([thisResultRef.remove()])
}

/*
Check if this user has submitted a results for this group already.
If no result has been submitted yet, set userId in v2/groupsUsers.
Expand Down

0 comments on commit 061282a

Please sign in to comment.