├── .babelrc ├── .dockerignore ├── .eslintrc.json ├── .github └── workflows │ ├── docker-image.yml │ ├── node-test.yml │ └── node.js.yml ├── .gitignore ├── .mocharc.yaml ├── .npmrc ├── .nycrc.json ├── CHANGELOG.md ├── Dockerfile ├── EVENTS.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── api └── openapi-spec │ └── openapi.yaml ├── docker-compose ├── docker-compose-test-audit.yml ├── docker-compose-test-inmemory.yml ├── docker-compose-test-mariadb-redis-core-cluster.yml ├── docker-compose-test-mariadb-redis-core.yml ├── docker-compose-test-mariadb-redis.yml ├── docker-compose-test-mariadb.yml ├── docker-compose-test-memcached.yml ├── docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml ├── docker-compose-test-mysql-innodbcluster-redis.yml ├── docker-compose-test-mysql.yml ├── docker-compose-test-signed.yml ├── docker-compose-test.yml ├── scripts │ ├── db.sql │ └── setupCluster.js └── test_env ├── env.sample ├── examples ├── cloud-init │ └── cloud-init.yml ├── docker-compose │ ├── docker-compose-memcached.yml │ ├── docker-compose-redis.yml │ ├── docker-compose.yml │ ├── sample.env │ ├── traefik.sample.toml │ └── traefik.toml └── vagrant │ ├── .vagrant │ ├── machines │ │ ├── client2 │ │ │ └── virtualbox │ │ │ │ └── vagrant_cwd │ │ └── web │ │ │ └── virtualbox │ │ │ └── vagrant_cwd │ ├── provisioners │ │ └── ansible │ │ │ └── inventory │ │ │ └── vagrant_ansible_inventory │ └── rgloader │ │ └── loader.rb │ ├── Vagrantfile │ └── provisioning │ ├── client.yml │ ├── manager.yml │ └── server.yml ├── package-lock.json ├── package.json ├── runTestsAll.sh ├── runTestsCluster.sh ├── runTestsDocker.sh ├── runTestsUpgrade.sh ├── scripts ├── buildDockerImage.sh ├── buildDockerImages.sh ├── getVersion.sh └── updateVersion.sh ├── settings.json.sample ├── src ├── appenv.js ├── cacheInitiator.js ├── dbInitiator.js ├── index.js ├── lib │ ├── cache │ │ ├── inmemory │ │ │ └── index.js │ │ ├── memcached │ │ │ └── index.js │ │ ├── modules.js │ │ └── redis │ │ │ └── index.js │ ├── helpers │ │ ├── AdminHelper.js │ │ ├── AppHelper.js │ │ ├── AuditHelper.js │ │ ├── CacheHelper.js │ │ ├── DbHelper.js │ │ ├── EventHelper.js │ │ ├── ImpExpHelper.js │ │ ├── KeysHelper.js │ │ ├── PluginHelper.js │ │ ├── RemoteLoggerHelper.js │ │ ├── UpdateHelper.js │ │ └── admins │ │ │ ├── AdminAccountHelper.js │ │ │ ├── AdminGroupAccountHelper.js │ │ │ ├── AdminGroupHelper.js │ │ │ ├── AdminGroupPermissionHelper.js │ │ │ ├── AdminKeyHelper.js │ │ │ └── AdminPermissionHelper.js │ ├── keys_importer │ │ ├── github │ │ │ └── index.js │ │ ├── gitlab │ │ │ └── index.js │ │ └── modules.js │ ├── managers │ │ ├── AccountManager.js │ │ ├── AuthTokenManager.js │ │ ├── BaseCacheManager.js │ │ ├── CacheManager.js │ │ ├── DbManager.js │ │ ├── GroupAccountManager.js │ │ ├── GroupManager.js │ │ ├── KeyManager.js │ │ ├── KeysImporterManager.js │ │ └── PermissionManager.js │ ├── middlewares │ │ ├── AuditMiddleware.js │ │ └── AuthMiddleware.js │ ├── rdbms │ │ ├── baseclient.js │ │ ├── mariadb │ │ │ └── index.js │ │ ├── modules.js │ │ └── sqlite │ │ │ ├── client.js │ │ │ └── index.js │ └── utils │ │ ├── cryptoUtils.js │ │ ├── dateUtils.js │ │ ├── dnsUtils.js │ │ ├── fsUtils.js │ │ ├── httpUtils.js │ │ ├── logUtils.js │ │ ├── processUtils.js │ │ ├── sshOptionsUtils.js │ │ └── sshUtils.js ├── migrations │ ├── v10fixGroups.js │ ├── v12fixFingerprints.js │ └── v7fixGroups.js ├── routes │ ├── accounts.js │ ├── groups.js │ ├── impexp.js │ ├── index.js │ ├── keys.js │ └── permissions.js ├── settingsInitiator.js └── theoserver.js ├── tests ├── Mocks.js ├── accounts.json ├── groups.json ├── testCluster.js ├── testCore.js ├── testCoreNoAssignee.js ├── testCoreOnRestart.js ├── testCoreOnRestartNoAssignee.js ├── testCoreTokens.js ├── testCoreTokensNoAssignee.js ├── testRestAdmin.js ├── testRestKeys.js ├── testSignedRestAdmin.js ├── testStandAlone_aAdmin.js ├── testStandAlone_a_HTTPUtils.js ├── testStandAlone_a_SSHOptionsUtils.js ├── testStandAlone_a_SSHUtils.js ├── testStandAlone_a_cache.js ├── testStandAlone_a_cryptoUtils.js ├── testStandAlone_a_dateUtils.js ├── testStandAlone_a_logUtils.js ├── testStandAlone_a_processUtils.js ├── testStandAlone_a_rdbms.js ├── testStandAlone_bKeys.js ├── testStandAlone_zAdminSignedKeys.js ├── testUpgrade_mysql.js └── testUpgrade_sqlite.js └── theo-logo-500.png /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [[ 3 | "@babel/preset-env", 4 | { 5 | "targets": { 6 | "node": "current" 7 | } 8 | } 9 | ]], 10 | "env": { 11 | "test": { 12 | "plugins": ["istanbul"] 13 | } 14 | }, 15 | "plugins": ["@babel/plugin-proposal-class-properties"] 16 | } 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .eslintrc.json 3 | .git 4 | .gitignore 5 | .idea 6 | .npmrc 7 | node_modules 8 | build 9 | *.md 10 | data 11 | env.sample 12 | docker-compose 13 | dev 14 | *.sh 15 | tmp.* 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "prettier" 5 | ], 6 | "plugins": [ 7 | "prettier" 8 | ], 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "no-undef": "warn", 12 | "no-use-before-define": "warn", 13 | "no-console": "off", 14 | "camelcase": "off", 15 | "prettier/prettier": [ 16 | "error", 17 | { 18 | "arrowParens": "avoid", 19 | "printWidth": 120, 20 | "tabWidth": 2, 21 | "singleQuote": true, 22 | "trailingComma": "none", 23 | "bracketSpacing": true, 24 | "parser": "flow", 25 | "semi": true 26 | } 27 | ] 28 | }, 29 | "parserOptions": { 30 | "ecmaVersion": 2016, 31 | "sourceType": "module" 32 | }, 33 | "env": { 34 | "es6": true, 35 | "node": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: release to docker hub 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Log into registry 14 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 15 | - name: build image and push 16 | run: ./scripts/buildDockerImages.sh all push 17 | env: 18 | DOCKER_IMAGE: ${{ secrets.DOCKER_IMAGE_NAME }} 19 | - name: call AK webhook 20 | run: | 21 | # Strip git ref prefix from version 22 | VERSION=$(./scripts/getVersion.sh) 23 | LATEST_TAG=$(git tag | grep -E '^[0-9]*\.[0-9]*\.[0-9]*$' | sort --version-sort | tail -n 1) 24 | if [ "$LATEST_TAG" = "$DOCKER_TAG" ]; then 25 | LATEST=true 26 | else 27 | LATEST=false 28 | fi 29 | if [ -n "${{ secrets.THEO_UPDATE_TOKEN }}" ]; then 30 | curl --fail -s -H "Content-Type: application/json" -H "Authorization: Bearer ${{ secrets.THEO_UPDATE_TOKEN }}" \ 31 | -d '{"code": "'$VERSION'", "latest": '${LATEST}' }' https://update.theo.authkeys.io/version 32 | fi 33 | - name: call microbadger webhook 34 | run: | 35 | if [ -n "${{ secrets.MICROBADGE_TOKEN }}" ]; then 36 | curl -XPOST https://hooks.microbadger.com/images/theoapp/theo/${{ secrets.MICROBADGE_TOKEN }} 37 | fi 38 | -------------------------------------------------------------------------------- /.github/workflows/node-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 14.x 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '14' 17 | - run: npm i -g npm 18 | - run: npm ci 19 | - run: npm run lint 20 | 21 | test: 22 | runs-on: ubuntu-20.04 23 | needs: lint 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js 14.x 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: '14' 30 | - run: npm i -g npm 31 | - run: npm ci --no-optional 32 | - run: npm run test:standalone 33 | 34 | build: 35 | runs-on: ubuntu-20.04 36 | needs: test 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Use Node.js 14.x 40 | uses: actions/setup-node@v2 41 | with: 42 | node-version: '14' 43 | - run: npm i -g npm 44 | - run: npm ci --no-optional 45 | - run: npm run build --if-present 46 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI e2e 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | full-test: 9 | 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Log into registry 14 | env: 15 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 16 | if: env.DOCKER_PASSWORD != null 17 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 18 | - run: ./runTestsAll.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | *.swp 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Dependency directory 24 | # https://docs.npmjs.com/cli/shrinkwrap#caveats 25 | node_modules 26 | 27 | # Debug log from npm 28 | npm-debug.log 29 | 30 | /settings.json 31 | 32 | /.idea 33 | /.env 34 | 35 | /build 36 | /node_modules 37 | /data 38 | /plugins 39 | 40 | /tmp* 41 | -------------------------------------------------------------------------------- /.mocharc.yaml: -------------------------------------------------------------------------------- 1 | require: '@babel/register' 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-babel", 3 | "temp-dir": "reports/coverage/.nyc_output", 4 | "report-dir": "reports/coverage" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Theo 2 | 3 | ## CHANGELOG 4 | 5 | ### 0.12.0 6 | 7 | * \+ Improved checks for REQUIRE_SIGNED_KEY 8 | * \+ Add new event to log HTTP request 9 | 10 | ### 0.11.0 11 | 12 | * \+ Add new env variable CLUSTER_MODE to support horizontal scaling of theo instances. 13 | NOTE: It requires mariadb/mysql database and redis 14 | * \+ Add new setting to accept only signed keys (env REQUIRE_SIGNED_KEY=1 or settings.keys.sign = 1) 15 | 16 | ### 0.10.0 17 | 18 | * Rename groups table to tgroups to support mysql-server 19 | * Container image now uses node:10-alpine 20 | * When using sqlite, Theo will try to create db parent directory (Since now it failed if such directory didn't exist) 21 | * \+ Build now uses multi-stage build 22 | * \+ GET /authorized_keys now accepts only 1 parameter (user) host is detected from client IP (or x-forwarded-for http header). 23 | NOTE we detected some issues when running theo in docker/k8s environment using IPv6 24 | 25 | 26 | ### 0.9.0 27 | 28 | * \+ Add cloud-init examples 29 | * \+ Documentation is now written in rtd and published to theoapp.readthedocs.io 30 | * \+ New feature to read/store admin and client tokens from db 31 | 32 | ### 0.8.0 33 | * \+ Exp/Imp functions 34 | 35 | ### 0.7.0 36 | * \+ Account expiration date 37 | * \+ New API to retrieve all accounts/groups with a specific permission 38 | 39 | ### 0.6.0 40 | * \+ Public key signature 41 | * \+ New permissions architecture. Permissions are now linked only to groups. When created, an account is member of a personal group with a name equals to account's email. 42 | * \+ Initial support to plugin. Plugin will extends Theo functionalities through events listeners. See [EVENTS](EVENTS.md) to check which events are available. 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # builder image 2 | FROM node:14-alpine AS builder 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY package*.json ./ 7 | 8 | RUN npm i -g npm && npm ci --no-optional 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | # production image 15 | FROM node:14-alpine 16 | 17 | ENV NODE_ENV=production 18 | 19 | WORKDIR /app 20 | 21 | COPY package*.json ./ 22 | 23 | RUN npm i -g npm && npm ci --no-optional &&\ 24 | npm cache clean --force 25 | 26 | COPY --from=builder /usr/src/app/build ./build/ 27 | 28 | EXPOSE 9100 29 | 30 | CMD [ "node", "build/index.js" ] 31 | 32 | # Metadata 33 | LABEL org.opencontainers.image.vendor="Authkeys" \ 34 | org.opencontainers.image.url="https://theo.authkeys.io" \ 35 | org.opencontainers.image.source="https://github.com/theoapp/theo-node" \ 36 | org.opencontainers.image.title="Theo Server" \ 37 | org.opencontainers.image.description="The authorized keys manager" \ 38 | org.opencontainers.image.version="1.3.0" \ 39 | org.opencontainers.image.documentation="https://theoapp.readthedocs.io" \ 40 | org.opencontainers.image.licenses='Apache-2.0' 41 | -------------------------------------------------------------------------------- /EVENTS.md: -------------------------------------------------------------------------------- 1 | # THEO EVENTS 2 | 3 | ## Authentication 4 | 5 | | event | arguments | callback | 6 | | ------| --------- | ------ | 7 | |theo:authorize | token, callback (err, auth) | `auth` : `false` if not authorized. `{is_admin: true/false, is_core: true/false}` | 8 | |theo:http-request | weblog, request, response | - | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Theo](theo-logo-500.png) 2 | 3 | [![](https://images.microbadger.com/badges/version/theoapp/theo.svg)](https://microbadger.com/images/theoapp/theo "theoapp/theo") 4 | [![](https://images.microbadger.com/badges/image/theoapp/theo.svg)](https://microbadger.com/images/theoapp/theo) 5 | [![Build Status](https://travis-ci.org/theoapp/theo-node.svg?branch=master)](https://travis-ci.org/theoapp/theo-node) 6 | [![Documentation Status](https://readthedocs.org/projects/theoapp/badge/?version=latest)](https://theoapp.readthedocs.io/en/latest/?badge=latest) 7 | 8 | Theo is the authorized keys manager, you can use it as replacement for all of your `authorized_keys` 9 | It allows you to set fine permissions (specific user and host) or to use wildcard (ex, using host `%.test.sample.com`) 10 | 11 | ### Summary 12 | 13 | - [System Requirements](#system-requirements) 14 | - [Prerequisites](#prerequisites) 15 | - [Documentation](#documentation) 16 | 17 | ### System Requirements 18 | 19 | Please check if your system meets the following minimum requirements : 20 | 21 | | Type | Value | 22 | | ---- | ------- | 23 | | CPU | 1 GHz | 24 | | RAM | 512 MiB | 25 | 26 | ### Prerequisites 27 | 28 | OpenSSH server must be version >= 6.2 (since it has to support `AuthorizedKeysCommand` option) 29 | 30 | ### Documentation 31 | 32 | Available at [theoapp.readthedocs.io](https://theoapp.readthedocs.io/en/latest/) 33 | 34 | ### Public Test Instance 35 | 36 | A public test instance is available at [theo.test.authkeys.io](https://theo.test.authkeys.io) 37 | 38 | Database will be reset every 6 hours (0am 6am 12pm 18pm UTC) 39 | 40 | Configured tokens: 41 | 42 | ```bash 43 | ADMIN_TOKEN=RMkqF4B8h6jtv3upvy3QubzNyTrMdgn8 44 | 45 | CLIENT_TOKENS=h8LYYwGgTqKFYQ3mRN2hv8vK5CBGJvMs,gAWXaG9ZnhHAXsDbF6dv3NYEbPNuZKR7 46 | 47 | ``` 48 | 49 | Instance has the `REQUIRE_SIGNED_KEY` flag on, so you need to enable key sign/verify on your side: 50 | 51 | * [Theo CLI](https://theoapp.readthedocs.io/en/latest/theo-cli/signed_keys.html) 52 | * [Theo agent](https://theoapp.readthedocs.io/en/latest/theo-agent/verify_signed_keys.html) 53 | 54 | **Be aware that the instance is public, so everyone has access to the data, please use fake email** 55 | 56 | ### Artwork 57 | 58 | Theo logo created by [Tomm](http://oyace.tumblr.com/) 59 | 60 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | * ~~Export/Import~~ (Released 0.8.0) 4 | * ~~cloud-init examples~~ (Released 0.8.3) 5 | * Tests with SSH key fingerprint -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-audit.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | theo: 5 | image: theo:test 6 | container_name: theo 7 | environment: 8 | - MODE=test 9 | - DB_STORAGE=":memory:" 10 | - ADMIN_TOKENS=${ADMIN_TOKENS_AUDIT} 11 | - CLIENT_TOKENS=${CLIENT_TOKENS} 12 | - LOG_AUDIT_CONSOLE=true 13 | - SKIP_UPDATECHECK=1 14 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-inmemory.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | theo: 5 | image: theo:test 6 | container_name: theo 7 | environment: 8 | - MODE=test 9 | - DB_STORAGE=":memory:" 10 | - ADMIN_TOKEN=${ADMIN_TOKEN} 11 | - CLIENT_TOKENS=${CLIENT_TOKENS} 12 | - CACHE_ENABLED=inmemory 13 | - SKIP_UPDATECHECK=1 14 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mariadb-redis-core-cluster.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: ${MARIADB_IMAGE} 6 | environment: 7 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 8 | MYSQL_DATABASE: ${MYSQL_DATABASE} 9 | MYSQL_USER: ${MYSQL_USER} 10 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 11 | cache: 12 | image: ${REDIS_IMAGE} 13 | theo1: 14 | image: theo:test 15 | container_name: theo1 16 | environment: 17 | MODE: test 18 | DB_ENGINE: mariadb 19 | DB_HOST: db 20 | DB_USER: ${MYSQL_USER} 21 | DB_PASSWORD: ${MYSQL_PASSWORD} 22 | DB_NAME: ${MYSQL_DATABASE} 23 | CORE_TOKEN: ${CORE_TOKEN} 24 | CACHE_ENABLED: redis 25 | CACHE_URI: redis://cache:6379 26 | CLUSTER_MODE: 1 27 | DB_CONN_MAX_RETRY: 30 28 | SKIP_UPDATECHECK: 1 29 | depends_on: 30 | - cache 31 | - db 32 | theo2: 33 | image: theo:test 34 | container_name: theo2 35 | environment: 36 | MODE: test 37 | DB_ENGINE: mariadb 38 | DB_HOST: db 39 | DB_USER: ${MYSQL_USER} 40 | DB_PASSWORD: ${MYSQL_PASSWORD} 41 | DB_NAME: ${MYSQL_DATABASE} 42 | CORE_TOKEN: ${CORE_TOKEN} 43 | CACHE_ENABLED: redis 44 | CACHE_URI: redis://cache:6379 45 | CLUSTER_MODE: 1 46 | DB_CONN_MAX_RETRY: 30 47 | SKIP_UPDATECHECK: 1 48 | depends_on: 49 | - cache 50 | - db 51 | theo3: 52 | image: theo:test 53 | container_name: theo3 54 | environment: 55 | MODE: test 56 | DB_ENGINE: mariadb 57 | DB_HOST: db 58 | DB_USER: ${MYSQL_USER} 59 | DB_PASSWORD: ${MYSQL_PASSWORD} 60 | DB_NAME: ${MYSQL_DATABASE} 61 | CORE_TOKEN: ${CORE_TOKEN} 62 | CACHE_ENABLED: redis 63 | CACHE_URI: redis://cache:6379 64 | CLUSTER_MODE: 1 65 | DB_CONN_MAX_RETRY: 30 66 | SKIP_UPDATECHECK: 1 67 | depends_on: 68 | - cache 69 | - db 70 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mariadb-redis-core.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: ${MARIADB_IMAGE} 6 | environment: 7 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 8 | MYSQL_DATABASE: ${MYSQL_DATABASE} 9 | MYSQL_USER: ${MYSQL_USER} 10 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 11 | cache: 12 | image: ${REDIS_IMAGE} 13 | theo: 14 | image: theo:test 15 | container_name: theo 16 | environment: 17 | MODE: test 18 | DB_ENGINE: mariadb 19 | DB_HOST: db 20 | DB_USER: ${MYSQL_USER} 21 | DB_PASSWORD: ${MYSQL_PASSWORD} 22 | DB_NAME: ${MYSQL_DATABASE} 23 | CORE_TOKEN: ${CORE_TOKEN} 24 | CACHE_ENABLED: redis 25 | CACHE_URI: redis://cache:6379 26 | DB_CONN_MAX_RETRY: 30 27 | SKIP_UPDATECHECK: 1 28 | depends_on: 29 | - cache 30 | - db 31 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mariadb-redis.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: ${MARIADB_IMAGE} 6 | environment: 7 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 8 | MYSQL_DATABASE: ${MYSQL_DATABASE} 9 | MYSQL_USER: ${MYSQL_USER} 10 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 11 | cache: 12 | image: ${REDIS_IMAGE} 13 | theo: 14 | image: theo:test 15 | container_name: theo 16 | environment: 17 | MODE: test 18 | DB_ENGINE: mariadb 19 | DB_HOST: db 20 | DB_USER: ${MYSQL_USER} 21 | DB_PASSWORD: ${MYSQL_PASSWORD} 22 | DB_NAME: ${MYSQL_DATABASE} 23 | ADMIN_TOKEN: ${ADMIN_TOKEN} 24 | CLIENT_TOKENS: ${CLIENT_TOKENS} 25 | CACHE_ENABLED: redis 26 | CACHE_URI: redis://cache:6379 27 | DB_CONN_MAX_RETRY: 30 28 | SKIP_UPDATECHECK: 1 29 | depends_on: 30 | - cache 31 | - db 32 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mariadb.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: ${MARIADB_IMAGE} 6 | environment: 7 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 8 | MYSQL_DATABASE: ${MYSQL_DATABASE} 9 | MYSQL_USER: ${MYSQL_USER} 10 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 11 | theo: 12 | image: theo:test 13 | container_name: theo 14 | environment: 15 | MODE: test 16 | DB_ENGINE: mariadb 17 | DB_HOST: db 18 | DB_USER: ${MYSQL_USER} 19 | DB_PASSWORD: ${MYSQL_PASSWORD} 20 | DB_NAME: ${MYSQL_DATABASE} 21 | ADMIN_TOKEN: ${ADMIN_TOKEN} 22 | CLIENT_TOKENS: ${CLIENT_TOKENS} 23 | DB_CONN_MAX_RETRY: 30 24 | SKIP_UPDATECHECK: 1 25 | depends_on: 26 | - db 27 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-memcached.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | cache: 5 | image: ${MEMCACHED_IMAGE} 6 | theo: 7 | image: theo:test 8 | container_name: theo 9 | environment: 10 | - MODE=test 11 | - DB_STORAGE=":memory:" 12 | - ADMIN_TOKEN=${ADMIN_TOKEN} 13 | - CLIENT_TOKENS=${CLIENT_TOKENS} 14 | - CACHE_ENABLED=memcached 15 | - CACHE_URI=cache:11211 16 | - SKIP_UPDATECHECK=1 17 | depends_on: 18 | - cache 19 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | mysql-server-1: 5 | image: ${MYSQLSERVER_IMAGE} 6 | command: 7 | - "mysqld" 8 | - "--server_id=1" 9 | - "--binlog_checksum=NONE" 10 | - "--gtid_mode=ON" 11 | - "--enforce_gtid_consistency=ON" 12 | - "--log_bin" 13 | - "--log_slave_updates=ON" 14 | - "--master_info_repository=TABLE" 15 | - "--relay_log_info_repository=TABLE" 16 | - "--transaction_write_set_extraction=XXHASH64" 17 | - "--user=mysql" 18 | - "--skip-host-cache" 19 | - "--skip-name-resolve" 20 | - "--default_authentication_plugin=mysql_native_password" 21 | environment: 22 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 23 | MYSQL_ROOT_HOST: "%" 24 | mysql-server-2: 25 | image: ${MYSQLSERVER_IMAGE} 26 | command: 27 | - "mysqld" 28 | - "--server_id=2" 29 | - "--binlog_checksum=NONE" 30 | - "--gtid_mode=ON" 31 | - "--enforce_gtid_consistency=ON" 32 | - "--log_bin" 33 | - "--log_slave_updates=ON" 34 | - "--master_info_repository=TABLE" 35 | - "--relay_log_info_repository=TABLE" 36 | - "--transaction_write_set_extraction=XXHASH64" 37 | - "--user=mysql" 38 | - "--skip-host-cache" 39 | - "--skip-name-resolve" 40 | - "--default_authentication_plugin=mysql_native_password" 41 | environment: 42 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 43 | MYSQL_ROOT_HOST: "%" 44 | mysql-server-3: 45 | image: ${MYSQLSERVER_IMAGE} 46 | command: 47 | - "mysqld" 48 | - "--server_id=3" 49 | - "--binlog_checksum=NONE" 50 | - "--gtid_mode=ON" 51 | - "--enforce_gtid_consistency=ON" 52 | - "--log_bin" 53 | - "--log_slave_updates=ON" 54 | - "--master_info_repository=TABLE" 55 | - "--relay_log_info_repository=TABLE" 56 | - "--transaction_write_set_extraction=XXHASH64" 57 | - "--user=mysql" 58 | - "--skip-host-cache" 59 | - "--skip-name-resolve" 60 | - "--default_authentication_plugin=mysql_native_password" 61 | environment: 62 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 63 | MYSQL_ROOT_HOST: "%" 64 | mysql-shell: 65 | image: neumayer/mysql-shell-batch 66 | volumes: 67 | - ./scripts/:/scripts/ 68 | depends_on: 69 | - mysql-server-1 70 | - mysql-server-2 71 | - mysql-server-3 72 | environment: 73 | MYSQL_USER: root 74 | MYSQL_HOST: mysql-server-1 75 | MYSQL_PORT: 3306 76 | MYSQL_PASSWORD: ${MYSQL_ROOT_PASSWORD} 77 | MYSQLSH_SCRIPT: /scripts/setupCluster.js 78 | MYSQL_SCRIPT: /scripts/db.sql 79 | cache: 80 | image: ${REDIS_IMAGE} 81 | theo1: 82 | image: theo:test 83 | container_name: theo1 84 | restart: always 85 | environment: 86 | MODE: test 87 | LOG_LEVEL: debug 88 | DB_ENGINE: mariadb 89 | DB_CLUSTER: rw:mysql-server-1:3306,ro:mysql-server-2:3306,ro:mysql-server-2:3306 90 | DB_USER: ${MYSQL_USER} 91 | DB_PASSWORD: ${MYSQL_PASSWORD} 92 | DB_NAME: ${MYSQL_DATABASE} 93 | CORE_TOKEN: ${CORE_TOKEN} 94 | CACHE_ENABLED: redis 95 | CACHE_URI: redis://cache:6379 96 | CLUSTER_MODE: 1 97 | DB_CONN_MAX_RETRY: 30 98 | SKIP_UPDATECHECK: 1 99 | depends_on: 100 | - cache 101 | - mysql-server-1 102 | - mysql-server-2 103 | - mysql-server-3 104 | theo2: 105 | image: theo:test 106 | container_name: theo2 107 | restart: always 108 | environment: 109 | MODE: test 110 | LOG_LEVEL: debug 111 | DB_ENGINE: mariadb 112 | DB_CLUSTER: rw:mysql-server-1:3306,ro:mysql-server-2:3306,ro:mysql-server-2:3306 113 | DB_USER: ${MYSQL_USER} 114 | DB_PASSWORD: ${MYSQL_PASSWORD} 115 | DB_NAME: ${MYSQL_DATABASE} 116 | CORE_TOKEN: ${CORE_TOKEN} 117 | CACHE_ENABLED: redis 118 | CACHE_URI: redis://cache:6379 119 | CLUSTER_MODE: 1 120 | DB_CONN_MAX_RETRY: 30 121 | SKIP_UPDATECHECK: 1 122 | depends_on: 123 | - cache 124 | - mysql-server-1 125 | - mysql-server-2 126 | - mysql-server-3 127 | theo3: 128 | image: theo:test 129 | container_name: theo3 130 | restart: always 131 | environment: 132 | MODE: test 133 | LOG_LEVEL: debug 134 | DB_ENGINE: mariadb 135 | DB_CLUSTER: rw:mysql-server-1:3306,ro:mysql-server-2:3306,ro:mysql-server-2:3306 136 | DB_USER: ${MYSQL_USER} 137 | DB_PASSWORD: ${MYSQL_PASSWORD} 138 | DB_NAME: ${MYSQL_DATABASE} 139 | CORE_TOKEN: ${CORE_TOKEN} 140 | CACHE_ENABLED: redis 141 | CACHE_URI: redis://cache:6379 142 | CLUSTER_MODE: 1 143 | DB_CONN_MAX_RETRY: 30 144 | SKIP_UPDATECHECK: 1 145 | depends_on: 146 | - cache 147 | - mysql-server-1 148 | - mysql-server-2 149 | - mysql-server-3 150 | 151 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mysql-innodbcluster-redis.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | mysql-server-1: 5 | image: ${MYSQLSERVER_IMAGE} 6 | command: 7 | - "mysqld" 8 | - "--server_id=1" 9 | - "--binlog_checksum=NONE" 10 | - "--gtid_mode=ON" 11 | - "--enforce_gtid_consistency=ON" 12 | - "--log_bin" 13 | - "--log_slave_updates=ON" 14 | - "--master_info_repository=TABLE" 15 | - "--relay_log_info_repository=TABLE" 16 | - "--transaction_write_set_extraction=XXHASH64" 17 | - "--user=mysql" 18 | - "--skip-host-cache" 19 | - "--skip-name-resolve" 20 | - "--default_authentication_plugin=mysql_native_password" 21 | environment: 22 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 23 | MYSQL_ROOT_HOST: "%" 24 | ports: 25 | - 3301:3306 26 | mysql-server-2: 27 | image: ${MYSQLSERVER_IMAGE} 28 | command: 29 | - "mysqld" 30 | - "--server_id=2" 31 | - "--binlog_checksum=NONE" 32 | - "--gtid_mode=ON" 33 | - "--enforce_gtid_consistency=ON" 34 | - "--log_bin" 35 | - "--log_slave_updates=ON" 36 | - "--master_info_repository=TABLE" 37 | - "--relay_log_info_repository=TABLE" 38 | - "--transaction_write_set_extraction=XXHASH64" 39 | - "--user=mysql" 40 | - "--skip-host-cache" 41 | - "--skip-name-resolve" 42 | - "--default_authentication_plugin=mysql_native_password" 43 | environment: 44 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 45 | MYSQL_ROOT_HOST: "%" 46 | ports: 47 | - 3302:3306 48 | mysql-server-3: 49 | image: ${MYSQLSERVER_IMAGE} 50 | command: 51 | - "mysqld" 52 | - "--server_id=3" 53 | - "--binlog_checksum=NONE" 54 | - "--gtid_mode=ON" 55 | - "--enforce_gtid_consistency=ON" 56 | - "--log_bin" 57 | - "--log_slave_updates=ON" 58 | - "--master_info_repository=TABLE" 59 | - "--relay_log_info_repository=TABLE" 60 | - "--transaction_write_set_extraction=XXHASH64" 61 | - "--user=mysql" 62 | - "--skip-host-cache" 63 | - "--skip-name-resolve" 64 | - "--default_authentication_plugin=mysql_native_password" 65 | environment: 66 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 67 | MYSQL_ROOT_HOST: "%" 68 | ports: 69 | - 3303:3306 70 | mysql-shell: 71 | image: neumayer/mysql-shell-batch 72 | volumes: 73 | - ./scripts/:/scripts/ 74 | depends_on: 75 | - mysql-server-1 76 | - mysql-server-2 77 | - mysql-server-3 78 | environment: 79 | MYSQL_USER: root 80 | MYSQL_HOST: mysql-server-1 81 | MYSQL_PORT: 3306 82 | MYSQL_PASSWORD: ${MYSQL_ROOT_PASSWORD} 83 | MYSQLSH_SCRIPT: /scripts/setupCluster.js 84 | MYSQL_SCRIPT: /scripts/db.sql 85 | cache: 86 | image: ${REDIS_IMAGE} 87 | ports: 88 | - 6379:6379 89 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-mysql.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: ${MYSQLSERVER_IMAGE} 6 | container_name: mysql-server 7 | command: ["mysqld","--skip-host-cache","--skip-name-resolve"] 8 | environment: 9 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 10 | theo: 11 | image: theo:test 12 | container_name: theo 13 | environment: 14 | MODE: test 15 | DB_ENGINE: mysql 16 | DB_HOST: db 17 | DB_USER: ${MYSQL_USER} 18 | DB_PASSWORD: ${MYSQL_PASSWORD} 19 | DB_NAME: ${MYSQL_DATABASE} 20 | ADMIN_TOKEN: ${ADMIN_TOKEN} 21 | CLIENT_TOKENS: ${CLIENT_TOKENS} 22 | DB_CONN_MAX_RETRY: 30 23 | SKIP_UPDATECHECK: 1 24 | depends_on: 25 | - db 26 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test-signed.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | theo: 5 | image: theo:test 6 | container_name: theo 7 | environment: 8 | - MODE=test 9 | - DB_STORAGE=":memory:" 10 | - ADMIN_TOKEN=${ADMIN_TOKEN} 11 | - CLIENT_TOKENS=${CLIENT_TOKENS} 12 | - REQUIRE_SIGNED_KEY=1 13 | - SKIP_UPDATECHECK=1 14 | -------------------------------------------------------------------------------- /docker-compose/docker-compose-test.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | theo: 5 | image: theo:test 6 | container_name: theo 7 | environment: 8 | - MODE=test 9 | - DB_STORAGE=":memory:" 10 | - ADMIN_TOKEN=${ADMIN_TOKEN} 11 | - CLIENT_TOKENS=${CLIENT_TOKENS} 12 | - SKIP_UPDATECHECK=1 13 | -------------------------------------------------------------------------------- /docker-compose/scripts/db.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE theo; 2 | CREATE USER 'theouser'@'%' IDENTIFIED WITH caching_sha2_password BY 'theopwd'; 3 | GRANT ALL PRIVILEGES ON theo.* TO 'theouser'@'%'; 4 | -------------------------------------------------------------------------------- /docker-compose/scripts/setupCluster.js: -------------------------------------------------------------------------------- 1 | var dbPass = "ex4mple" 2 | var clusterName = "devCluster" 3 | 4 | try { 5 | print('Setting up InnoDB cluster...\n'); 6 | shell.connect('root@mysql-server-1:3306', dbPass) 7 | var cluster = dba.createCluster(clusterName); 8 | print('Adding instances to the cluster.'); 9 | cluster.addInstance({user: "root", host: "mysql-server-2", password: dbPass}) 10 | print('.'); 11 | cluster.addInstance({user: "root", host: "mysql-server-3", password: dbPass}) 12 | print('.\nInstances successfully added to the cluster.'); 13 | print('\nInnoDB cluster deployed successfully.\n'); 14 | } catch(e) { 15 | print('\nThe InnoDB cluster could not be created.\n\nError: ' + e.message + '\n'); 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose/test_env: -------------------------------------------------------------------------------- 1 | # docker images 2 | export MARIADB_IMAGE=mariadb:10.5 3 | export MYSQLSERVER_IMAGE=mysql/mysql-server:8.0 4 | export REDIS_IMAGE=redis:6.0 5 | export MEMCACHED_IMAGE=memcached:1-alpine 6 | 7 | # mariadb/mysql 8 | 9 | export MYSQL_ROOT_PASSWORD=ex4mple 10 | export MYSQL_DATABASE=theo 11 | export MYSQL_USER=theouser 12 | export MYSQL_PASSWORD=theopwd 13 | 14 | # tokens 15 | export CORE_TOKEN=SuperSecretT0ken 16 | export ADMIN_TOKEN=ch4ng3Me 17 | export CLIENT_TOKENS=njknsjd2412fnjkasnj,knkjnknfjfnjenkln 18 | 19 | export ADMIN_TOKEN_AUDIT=jjkfsdbb23 20 | export ADMIN_TOKENS_AUDIT=4getm3N0t:admin1,n0t4ever:admin2,jjkfsdbb23:admin3 21 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | 2 | ADMIN_TOKEN=TMP_ADMIN_TOKEN 3 | CLIENT_TOKENS=TMP_CLIENT_TOKEN_1,TMP_CLIENT_TOKEN_2 4 | #REQUIRE_SIGNED_KEY=1 5 | #LOG_AUTH_KEYS_URL=http://remote/log 6 | #LOG_AUTH_KEYS_TOKEN= 7 | 8 | ########################## 9 | # 10 | # DATABASE 11 | # default: sqlite 12 | # 13 | ########################## 14 | 15 | #DB_ENGINE=sqlite 16 | #DB_STORAGE=data/theo.db 17 | 18 | #DB_ENGINE=mariadb 19 | #DB_HOST=127.0.0.1 20 | #DB_USER=DATABASE_USERNAME 21 | #DB_PASSWORD=DATABASE_PASSWORD 22 | #DB_NAME=DATABASE_NAME 23 | 24 | ########################## 25 | # 26 | # CACHE 27 | # default: disabled 28 | # 29 | ########################## 30 | 31 | #CACHE_ENABLED=memcached 32 | #CACHE_URI=localhost:11211 33 | #CACHE_OPTIONS= 34 | 35 | #CACHE_ENABLED=redis 36 | #CACHE_URI=redis://localhost:6379 37 | #CACHE_OPTIONS= 38 | -------------------------------------------------------------------------------- /examples/cloud-init/cloud-init.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | users: 4 | - name: theo-agent 5 | shell: /bin/false 6 | 7 | write_files: 8 | - path: /usr/sbin/download-install-theo.sh 9 | owner: root:root 10 | permissions: '0755' 11 | content: | 12 | #!/bin/bash 13 | curl -L -s -o /usr/sbin/theo-agent \ 14 | https://github.com/theoapp/theo-agent/releases/download/$(curl -L -s -H 'Accept: application/json' https://github.com/theoapp/theo-agent/releases/latest |sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/')/theo-agent-linux-amd64 15 | chmod 755 /usr/sbin/theo-agent 16 | # Replace THEO_APP_URL and VALID_CLIENT_TOKEN accordingly with you installation 17 | /usr/sbin/theo-agent -install -no-interactive -sshd-config -url THEO_APP_URL -token VALID_CLIENT_TOKEN 18 | 19 | runcmd: 20 | - /usr/sbin/download-install-theo.sh 21 | - systemctl restart ssh.service 22 | 23 | -------------------------------------------------------------------------------- /examples/docker-compose/docker-compose-memcached.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | # IPv4 only 4 | # docker network create http_network 5 | 6 | # IPv4/IPv6 network 7 | # docker network create http_network --ipv6 --subnet "fd00:0000:0000:0000::/64" 8 | 9 | networks: 10 | http_network: 11 | external: true 12 | cache_network: 13 | external: false 14 | services: 15 | memcached: 16 | image: memcached:1.5-alpine 17 | container_name: theo_cache 18 | restart: ${RESTART_MODE} 19 | networks: 20 | - cache_network 21 | theo: 22 | image: theoapp/theo 23 | container_name: theo 24 | restart: ${RESTART_MODE} 25 | labels: 26 | - traefik.enable=true 27 | - traefik.port=9100 28 | - traefik.frontend.rule=Host:${THEO_FQDN} 29 | - traefik.docker.network=http_network 30 | environment: 31 | - DB_ENGINE 32 | - DB_STORAGE 33 | - ADMIN_TOKEN 34 | - CLIENT_TOKENS 35 | - CACHE_ENABLED 36 | - CACHE_URI 37 | volumes: 38 | - ${VOLUMES_ROOT_PATH}/data:/data 39 | networks: 40 | - cache_network 41 | - http_network 42 | traefik: 43 | image: traefik:1.7-alpine 44 | container_name: traefik 45 | restart: ${RESTART_MODE} 46 | ports: 47 | - "80:80" 48 | - "443:443" 49 | volumes: 50 | - /var/run/docker.sock:/var/run/docker.sock:ro 51 | - ${VOLUMES_ROOT_PATH}/traefik.toml:/traefik.toml:ro 52 | - ${VOLUMES_ROOT_PATH}/acme:/etc/traefik/acme 53 | networks: 54 | - http_network 55 | -------------------------------------------------------------------------------- /examples/docker-compose/docker-compose-redis.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | # IPv4 only 4 | # docker network create http_network 5 | 6 | # IPv4/IPv6 network 7 | # docker network create http_network --ipv6 --subnet "fd00:0000:0000:0000::/64" 8 | 9 | networks: 10 | http_network: 11 | external: true 12 | cache_network: 13 | external: false 14 | services: 15 | redis: 16 | image: redis:4.0-alpine 17 | container_name: theo_cache 18 | restart: ${RESTART_MODE} 19 | networks: 20 | - cache_network 21 | theo: 22 | image: theoapp/theo 23 | container_name: theo 24 | restart: ${RESTART_MODE} 25 | labels: 26 | - traefik.enable=true 27 | - traefik.port=9100 28 | - traefik.frontend.rule=Host:${THEO_FQDN} 29 | - traefik.docker.network=http_network 30 | environment: 31 | - DB_ENGINE 32 | - DB_STORAGE 33 | - ADMIN_TOKEN 34 | - CLIENT_TOKENS 35 | - CACHE_ENABLED 36 | - CACHE_URI 37 | volumes: 38 | - ${VOLUMES_ROOT_PATH}/data:/data 39 | networks: 40 | - cache_network 41 | - http_network 42 | traefik: 43 | image: traefik:1.7-alpine 44 | container_name: traefik 45 | restart: ${RESTART_MODE} 46 | ports: 47 | - "80:80" 48 | - "443:443" 49 | volumes: 50 | - /var/run/docker.sock:/var/run/docker.sock:ro 51 | - ${VOLUMES_ROOT_PATH}/traefik.toml:/traefik.toml:ro 52 | - ${VOLUMES_ROOT_PATH}/acme:/etc/traefik/acme 53 | networks: 54 | - http_network 55 | -------------------------------------------------------------------------------- /examples/docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | # IPv4 only 4 | # docker network create http_network 5 | 6 | # IPv4/IPv6 network 7 | # docker network create http_network --ipv6 --subnet "fd00:0000:0000:0000::/64" 8 | 9 | networks: 10 | http_network: 11 | external: true 12 | 13 | services: 14 | theo: 15 | image: theoapp/theo 16 | container_name: theo 17 | restart: ${RESTART_MODE} 18 | labels: 19 | - traefik.enable=true 20 | - traefik.port=9100 21 | - traefik.frontend.rule=Host:${THEO_FQDN} 22 | - traefik.docker.network=http_network 23 | environment: 24 | - DB_ENGINE 25 | - DB_STORAGE 26 | - ADMIN_TOKEN 27 | - CLIENT_TOKENS 28 | volumes: 29 | - ${VOLUMES_ROOT_PATH}/data:/data 30 | networks: 31 | - http_network 32 | traefik: 33 | image: traefik:1.7-alpine 34 | container_name: traefik 35 | restart: ${RESTART_MODE} 36 | ports: 37 | - "80:80" 38 | - "443:443" 39 | labels: 40 | - traefik.enable=true 41 | - traefik.docker.network=http_network 42 | volumes: 43 | - /var/run/docker.sock:/var/run/docker.sock:ro 44 | - ${VOLUMES_ROOT_PATH}/traefik.toml:/traefik.toml:ro 45 | - ${VOLUMES_ROOT_PATH}/acme:/etc/traefik/acme 46 | networks: 47 | - http_network -------------------------------------------------------------------------------- /examples/docker-compose/sample.env: -------------------------------------------------------------------------------- 1 | # FQDN of theo server. 2 | THEO_FQDN= 3 | 4 | DB_ENGINE=sqlite 5 | DB_STORAGE=/data/theo.db 6 | 7 | # Admin token. Ex: RMkqF4B8h6jtv3upvy3QubzNyTrMdgn8 8 | ADMIN_TOKEN= 9 | 10 | # Client tokens, comma separated. Ex: h8LYYwGgTqKFYQ3mRN2hv8vK5CBGJvMs,gAWXaG9ZnhHAXsDbF6dv3NYEbPNuZKR7 11 | CLIENT_TOKENS= 12 | 13 | #CACHE_ENABLED=memcached 14 | #CACHE_URI=localhost:11211 15 | 16 | #CACHE_ENABLED=redis 17 | #CACHE_URI=redis://localhost:6379 18 | 19 | # Docker volumes parent folder 20 | VOLUMES_ROOT_PATH=/mnt/docker/theo 21 | 22 | # Docker containers restart mode 23 | # https://docs.docker.com/compose/compose-file/#restart 24 | RESTART_MODE=unless-stopped 25 | -------------------------------------------------------------------------------- /examples/docker-compose/traefik.sample.toml: -------------------------------------------------------------------------------- 1 | defaultEntryPoints = ["https","http"] 2 | 3 | [api] 4 | entryPoint = "traefik" 5 | dashboard = true 6 | 7 | [entryPoints] 8 | [entryPoints.http] 9 | address = ":80" 10 | [entryPoints.http.redirect] 11 | entryPoint = "https" 12 | [entryPoints.https] 13 | address = ":443" 14 | [entryPoints.https.tls] 15 | minVersion = "VersionTLS12" 16 | cipherSuites = [ 17 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", 18 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 19 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 20 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 21 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 22 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" 23 | ] 24 | 25 | [acme] 26 | email = "{{ EMAIL }}" 27 | storage = "/etc/traefik/acme/acme.json" 28 | entryPoint = "https" 29 | onHostRule = true 30 | [acme.tlsChallenge] 31 | 32 | [docker] 33 | endpoint = "unix:///var/run/docker.sock" 34 | domain = "{{ DOMAIN }}" 35 | watch = true 36 | exposedbydefault = false 37 | -------------------------------------------------------------------------------- /examples/docker-compose/traefik.toml: -------------------------------------------------------------------------------- 1 | defaultEntryPoints = ["https","http"] 2 | 3 | [api] 4 | entryPoint = "traefik" 5 | dashboard = true 6 | 7 | [entryPoints] 8 | [entryPoints.http] 9 | address = ":80" 10 | [entryPoints.http.redirect] 11 | entryPoint = "https" 12 | [entryPoints.https] 13 | address = ":443" 14 | [entryPoints.https.tls] 15 | minVersion = "VersionTLS12" 16 | cipherSuites = [ 17 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", 18 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 19 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 20 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 21 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 22 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" 23 | ] 24 | [entryPoints.traefik] 25 | address = ":8080" 26 | [entryPoints.traefik.auth.basic] 27 | users = ["admin:$apr1$n85QLQOd$bgZdhEGvTstCM.phwbRlU."] 28 | 29 | [acme] 30 | email = "" # <-- Put here your email address 31 | storage = "/etc/traefik/acme/acme.json" 32 | entryPoint = "https" 33 | onHostRule = true 34 | onDemand = false 35 | [acme.httpChallenge] 36 | entryPoint = "http" 37 | 38 | [docker] 39 | endpoint = "unix:///var/run/docker.sock" 40 | domain = "{{ DOMAIN }}" 41 | watch = true 42 | exposedbydefault = false -------------------------------------------------------------------------------- /examples/vagrant/.vagrant/machines/client2/virtualbox/vagrant_cwd: -------------------------------------------------------------------------------- 1 | /Users/macno/Projects/TheoApp/theo-node/examples/vagrant -------------------------------------------------------------------------------- /examples/vagrant/.vagrant/machines/web/virtualbox/vagrant_cwd: -------------------------------------------------------------------------------- 1 | /Users/macno/Projects/TheoApp/theo-node/examples/vagrant -------------------------------------------------------------------------------- /examples/vagrant/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory: -------------------------------------------------------------------------------- 1 | # Generated by Vagrant 2 | 3 | client1 ansible_host=127.0.0.1 ansible_port=2222 ansible_user='vagrant' ansible_ssh_private_key_file='/Users/macno/Projects/TheoApp/theo-node/examples/vagrant/.vagrant/machines/client1/virtualbox/private_key' 4 | server ansible_host=127.0.0.1 ansible_port=2200 ansible_user='vagrant' ansible_ssh_private_key_file='/Users/macno/Projects/TheoApp/theo-node/examples/vagrant/.vagrant/machines/server/virtualbox/private_key' 5 | manager ansible_host=127.0.0.1 ansible_port=2201 ansible_user='vagrant' ansible_ssh_private_key_file='/Users/macno/Projects/TheoApp/theo-node/examples/vagrant/.vagrant/machines/manager/virtualbox/private_key' 6 | -------------------------------------------------------------------------------- /examples/vagrant/.vagrant/rgloader/loader.rb: -------------------------------------------------------------------------------- 1 | # This file loads the proper rgloader/loader.rb file that comes packaged 2 | # with Vagrant so that encoded files can properly run with Vagrant. 3 | 4 | if ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"] 5 | require File.expand_path( 6 | "rgloader/loader", ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"]) 7 | else 8 | raise "Encoded files can't be read outside of the Vagrant installer." 9 | end 10 | -------------------------------------------------------------------------------- /examples/vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | 3 | config.vm.define "server" do |server| 4 | server.vm.network :private_network, ip: "192.168.56.254" 5 | server.vm.hostname = "server" 6 | server.vm.provider :virtualbox do |v| 7 | v.customize ["modifyvm", :id, "--name", "server"] 8 | end 9 | server.vm.provision "ansible" do |ansible| 10 | ansible.compatibility_mode = "2.0" 11 | ansible.playbook = "provisioning/server.yml" 12 | end 13 | end 14 | 15 | config.vm.define "manager" do |manager| 16 | manager.vm.hostname = "manager" 17 | manager.vm.network :private_network, ip: "192.168.56.200" 18 | manager.vm.provider :virtualbox do |v| 19 | v.customize ["modifyvm", :id, "--name", "manager"] 20 | end 21 | manager.vm.provision "ansible" do |ansible| 22 | ansible.compatibility_mode = "2.0" 23 | ansible.playbook = "provisioning/manager.yml" 24 | end 25 | end 26 | 27 | (1..2).each do |i| 28 | config.vm.define "client#{i}" do |client| 29 | client.vm.hostname = "client#{i}" 30 | client.vm.network :private_network, ip: "192.168.56.10#{i}" 31 | client.vm.provider :virtualbox do |v| 32 | v.customize ["modifyvm", :id, "--name", "client#{i}"] 33 | v.linked_clone = true 34 | end 35 | client.vm.provision "ansible" do |ansible| 36 | ansible.compatibility_mode = "2.0" 37 | ansible.playbook = "provisioning/client.yml" 38 | end 39 | end 40 | end 41 | 42 | 43 | config.vm.box = "debian/stretch64" 44 | config.vm.provider :virtualbox do |vb| 45 | vb.customize ["modifyvm", :id, "--memory", "512"] 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /examples/vagrant/provisioning/client.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | become: yes 3 | tasks: 4 | - name: Add the user 'theo-agent' 5 | user: 6 | name: theo-agent 7 | comment: Theo Agent 8 | shell: /bin/false 9 | - name: Download theo-agent 10 | get_url: 11 | url: https://github.com/theoapp/theo-agent/releases/download/v0.5.1/theo-agent-linux-amd64 12 | dest: /usr/sbin/theo-agent 13 | mode: 0775 14 | - name: Add server to hosts file 15 | lineinfile: dest=/etc/hosts regexp='.*server$' line="192.168.56.254 server" state=present 16 | - name: Install theo agent 17 | shell: theo-agent -install -no-interactive -sshd-config -url http://server:9100 -token "demo-client-token" 18 | - name: reload service ssh, in all cases 19 | systemd: 20 | name: ssh 21 | state: reloaded 22 | -------------------------------------------------------------------------------- /examples/vagrant/provisioning/manager.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | 3 | tasks: 4 | - name: Download nvm 5 | get_url: 6 | url: https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh 7 | dest: /tmp/nvm-install.sh 8 | mode: 0775 9 | - name: Install nvm 10 | shell: /tmp/nvm-install.sh 11 | 12 | - name: Install Node.js 13 | shell: '. {{ ansible_env.HOME }}/.nvm/nvm.sh && nvm install v8' 14 | args: 15 | creates: "{{ ansible_env.HOME }}/.nvm/versions/node" 16 | chdir: "{{ ansible_env.HOME }}" 17 | executable: /bin/bash 18 | - name: Add server to hosts file 19 | become: true 20 | lineinfile: dest=/etc/hosts regexp='.*server$' line="192.168.56.254 server" state=present 21 | - name: Create dir theo 22 | file: 23 | path: "{{ ansible_env.HOME }}/.theo" 24 | state: directory 25 | mode: 0775 26 | recurse: yes 27 | - name: Create theo settings 28 | file: 29 | path: "{{ ansible_env.HOME }}/.theo/env" 30 | state: touch 31 | - name: Setup theo-cli 32 | lineinfile: 33 | path: "{{ ansible_env.HOME }}/.theo/env" 34 | regexp: "^#? *{{ item.key | regex_escape() }}=" 35 | line: "{{ item.key }}={{ item.value }}" 36 | with_items: 37 | - { key: THEO_URL, value: "http://server:9100" } 38 | - { key: THEO_TOKEN, value: "demo-client-token" } 39 | -------------------------------------------------------------------------------- /examples/vagrant/provisioning/server.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | become: yes 3 | roles: 4 | - { role: "nickjj.docker", tags: ["docker"] } 5 | tasks: 6 | - name: Create dir 7 | file: 8 | path: /srv/theo/data 9 | state: directory 10 | owner: root 11 | group: root 12 | mode: 0775 13 | recurse: yes 14 | - name: run theo image 15 | shell: > 16 | docker run -d --name theo -p 9100:9100 \ 17 | -e DB_ENGINE=sqlite \ 18 | -e DB_STORAGE=/data/theo.sql \ 19 | -e ADMIN_TOKEN=demo-admin-token \ 20 | -e CLIENT_TOKENS=demo-client-token \ 21 | -v /srv/theo/data:/data \ 22 | theoapp/theo 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "theo", 3 | "version": "1.3.0", 4 | "description": "Theo is your AuthorizedKeys manager", 5 | "main": "index.js", 6 | "repository": "theoapp/theo-node", 7 | "scripts": { 8 | "build": "babel -D src -d build", 9 | "build:dev": "babel -D --watch src --out-dir build", 10 | "clean": "rm -rf build", 11 | "coverage": "cross-env NODE_ENV=test nyc mocha tests/testStandAlone*.js", 12 | "lint": "eslint src", 13 | "lint:fix": "eslint --fix src", 14 | "start:dev": "nodemon --signal SIGTERM --watch .env --watch src --exec babel-node ./src/index.js", 15 | "start": "node ./build/index.js", 16 | "test": "mocha tests/test*.js", 17 | "test:api": "mocha tests/testRest*", 18 | "test:api:signed": "mocha tests/testSigned*", 19 | "test:cluster": "mocha tests/testCluster.js", 20 | "test:core": "mocha tests/testCore.js", 21 | "test:core:restart": "mocha tests/testCoreOnRestart.js", 22 | "test:core:noassignee": "mocha tests/testCoreNoAssignee.js", 23 | "test:core:restart:noassignee": "mocha tests/testCoreOnRestartNoAssignee.js", 24 | "test:standalone": "mocha tests/testStandAlone*.js", 25 | "test:upgrade:mysql": "mocha tests/testUpgrade_mysql.js", 26 | "test:upgrade:sqlite": "mocha tests/testUpgrade_sqlite.js", 27 | "preversion": "npm run lint", 28 | "version": "./scripts/updateVersion.sh" 29 | }, 30 | "nodemonConfig": { 31 | "delay": 2000 32 | }, 33 | "author": "Michele Azzolari (macno)", 34 | "license": "Apache-2.0", 35 | "dependencies": { 36 | "@authkeys/microservice": "^2.4.3", 37 | "@authkeys/mysql-connman": "^0.6.0", 38 | "dotenv": "^8.2.0", 39 | "memcached": "^2.2.2", 40 | "node-fetch": "^2.6.1", 41 | "redis": "^3.0.0", 42 | "semver": "^7.3.4", 43 | "sqlite3": "^5.0.0", 44 | "sshpk": "^1.16.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/cli": "^7.13.0", 48 | "@babel/core": "^7.13.8", 49 | "@babel/eslint-parser": "^7.13.4", 50 | "@babel/node": "^7.13.0", 51 | "@babel/plugin-proposal-class-properties": "^7.13.0", 52 | "@babel/preset-env": "^7.13.8", 53 | "@babel/register": "^7.13.8", 54 | "@istanbuljs/nyc-config-babel": "^2.1.1", 55 | "babel-plugin-istanbul": "^5.1.4", 56 | "cross-env": "^5.2.0", 57 | "mocha": "^6.2.3", 58 | "mock-http-server": "^1.4.2", 59 | "nyc": "^14.1.1", 60 | "sinon": "^7.5.0" 61 | }, 62 | "optionalDependencies": { 63 | "eslint": "^7.20.0", 64 | "eslint-config-prettier": "^8.1.0", 65 | "eslint-config-standard": "^16.0.2", 66 | "eslint-plugin-prettier": "^3.3.1", 67 | "nodemon": "^2.0.7", 68 | "prettier": "^2.2.1" 69 | }, 70 | "nyc": { 71 | "include": "src", 72 | "exclude": "test" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /runTestsAll.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./runTestsDocker.sh 3 | RETVAL=$? 4 | if [[ ${RETVAL} -gt 0 ]]; then 5 | exit ${RETVAL} 6 | fi 7 | ./runTestsCluster.sh skip_build 8 | RETVAL=$? 9 | if [[ ${RETVAL} -gt 0 ]]; then 10 | exit ${RETVAL} 11 | fi 12 | ./runTestsUpgrade.sh skip_build 13 | RETVAL=$? 14 | exit ${RETVAL} 15 | -------------------------------------------------------------------------------- /runTestsCluster.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | WAIT_FOR_DB_SEC=20 4 | WAIT_FOR_DBCLUSTER_SEC=90 5 | if [ "$TRAVIS" = "true" ]; then 6 | WAIT_FOR_DB_SEC=60 7 | WAIT_FOR_DBCLUSTER_SEC=180 8 | fi 9 | 10 | print_test_header () { 11 | echo 12 | echo " ##### " 13 | echo $1 14 | echo " ##### " 15 | echo 16 | } 17 | 18 | if ! [[ "$1" == "skip_build" ]]; then 19 | # Build theo test image 20 | docker build --target builder -t theo-tester . 21 | RETVAL=$? 22 | if [[ ${RETVAL} -gt 0 ]]; then 23 | echo "ERR docker build FAILED" 24 | exit ${RETVAL} 25 | fi 26 | 27 | # Build theo server image 28 | docker build -t theo:test . 29 | RETVAL=$? 30 | if [[ ${RETVAL} -gt 0 ]]; then 31 | echo "ERR docker build FAILED" 32 | exit ${RETVAL} 33 | fi 34 | fi 35 | 36 | source ./docker-compose/test_env 37 | 38 | test_mariadb_redis_core_cluster() { 39 | print_test_header mariadb_redis_core_cluster 40 | docker-compose -p theotests -f docker-compose/docker-compose-test-mariadb-redis-core-cluster.yml up -d 41 | echo ${WAIT_FOR_DB_SEC}s Waiting for db to start.. 42 | sleep $WAIT_FOR_DB_SEC 43 | docker run --network theotests_default --rm --link theo \ 44 | -e "CORE_TOKEN=${CORE_TOKEN}" \ 45 | -e "THEO_URL_1=http://theo1:9100" \ 46 | -e "THEO_URL_2=http://theo2:9100" \ 47 | -e "THEO_URL_3=http://theo3:9100" \ 48 | theo-tester npm run test:cluster 49 | RETVAL=$? 50 | if [[ ${RETVAL} -gt 0 ]]; then 51 | echo "ERR docker-compose-test-mariadb-redis-core-cluster FAILED" 52 | docker-compose -p theotests -f docker-compose/docker-compose-test-mariadb-redis-core-cluster.yml logs --tail 50 theo1 53 | fi 54 | docker-compose -p theotests -f docker-compose/docker-compose-test-mariadb-redis-core-cluster.yml down 55 | if [[ ${RETVAL} -gt 0 ]]; then 56 | exit ${RETVAL} 57 | fi 58 | } 59 | 60 | test_mysql_innodbcluster_redis_core_cluster() { 61 | print_test_header mysql_innodbcluster_redis_core_cluster 62 | docker-compose -p theotests -f docker-compose/docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml up -d 63 | echo Waiting ${WAIT_FOR_DBCLUSTER_SEC}s for db to start.. 64 | sleep $WAIT_FOR_DBCLUSTER_SEC 65 | docker run --network theotests_default --rm --link theo \ 66 | -e "THEO_HOST=theo1" \ 67 | -e "THEO_PORT=9100" \ 68 | -e "CORE_TOKEN=${CORE_TOKEN}" \ 69 | -e "THEO_URL_1=http://theo1:9100" \ 70 | -e "THEO_URL_2=http://theo2:9100" \ 71 | -e "THEO_URL_3=http://theo3:9100" \ 72 | theo-tester npm run test:cluster 73 | RETVAL=$? 74 | if [[ ${RETVAL} -gt 0 ]]; then 75 | echo "ERR test_mysql_innodbcluster_redis_core_cluster FAILED" 76 | docker-compose -p theotests -f docker-compose/docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml logs --tail 50 theo1 77 | docker-compose -p theotests -f docker-compose/docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml logs --tail 50 theo2 78 | docker-compose -p theotests -f docker-compose/docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml logs --tail 50 theo3 79 | fi 80 | docker-compose -p theotests -f docker-compose/docker-compose-test-mysql-innodbcluster-redis-core-cluster.yml down 81 | if [[ ${RETVAL} -gt 0 ]]; then 82 | exit ${RETVAL} 83 | fi 84 | } 85 | 86 | test_mariadb_redis_core_cluster 87 | 88 | test_mysql_innodbcluster_redis_core_cluster 89 | -------------------------------------------------------------------------------- /runTestsUpgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | THEO_INITIAL_IMAGE=theoapp/theo:0.11.0 4 | 5 | if ! [[ "$1" == "skip_build" ]]; then 6 | # Build theo test image 7 | docker build --target builder -t theo-tester . 8 | RETVAL=$? 9 | if [[ ${RETVAL} -gt 0 ]]; then 10 | echo "ERR docker build FAILED" 11 | exit ${RETVAL} 12 | fi 13 | 14 | # Build theo server image 15 | docker build -t theo:test . 16 | RETVAL=$? 17 | if [[ ${RETVAL} -gt 0 ]]; then 18 | echo "ERR docker build FAILED" 19 | exit ${RETVAL} 20 | fi 21 | fi 22 | 23 | source ./docker-compose/test_env 24 | 25 | test_sqlite_upgrade() { 26 | mkdir -p tmp.$$ 27 | docker run --rm --name theo-sqlite-test -d -v $PWD/tmp.$$:/data -e DB_STORAGE=/data/theo.db $THEO_INITIAL_IMAGE 28 | echo "Wait 5 s.." 29 | sleep 5 30 | docker stop theo-sqlite-test 31 | docker run --rm --name theo-sqlite-test -d -v $PWD/tmp.$$:/data -e DB_STORAGE=/data/theo.db theo:test 32 | sleep 5 33 | docker stop theo-sqlite-test 34 | docker run --rm --name theo-sqlite-test -v $PWD/tmp.$$:/data -e DB_STORAGE=/data/theo.db theo-tester \ 35 | npm run test:upgrade:sqlite 36 | RETVAL=$? 37 | if [[ ${RETVAL} -gt 0 ]]; then 38 | echo "ERR sqlite upgrade FAILED" 39 | fi 40 | rm -rf tmp.$$ 41 | if [[ ${RETVAL} -gt 0 ]]; then 42 | exit ${RETVAL} 43 | fi 44 | } 45 | 46 | test_mysql_upgrade() { 47 | # start mysql 48 | echo "Starting mysql-server" 49 | docker run --rm --name theo-mysql-db -d \ 50 | -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \ 51 | ${MYSQLSERVER_IMAGE} \ 52 | mysqld --skip-host-cache --skip-name-resolve --default_authentication_plugin=mysql_native_password 53 | echo "Wait 60 s.." 54 | sleep 60 55 | docker exec theo-mysql-db mysql -uroot -p${MYSQL_ROOT_PASSWORD} \ 56 | -e "create database ${MYSQL_DATABASE}; create user ${MYSQL_USER}@'%' identified by '${MYSQL_PASSWORD}'; grant all on ${MYSQL_DATABASE}.* to ${MYSQL_USER}@'%';" 57 | # start theo old 58 | echo "Starting ${THEO_INITIAL_IMAGE}" 59 | docker run --rm -d --link theo-mysql-db --name theo-mysql-test \ 60 | -e DB_ENGINE=mysql \ 61 | -e DB_HOST=theo-mysql-db \ 62 | -e DB_USER=${MYSQL_USER} \ 63 | -e DB_PASSWORD=${MYSQL_PASSWORD} \ 64 | -e DB_NAME=${MYSQL_DATABASE} \ 65 | $THEO_INITIAL_IMAGE 66 | echo "Wait 10 s.." 67 | sleep 10 68 | docker stop theo-mysql-test 69 | echo "Starting current theo image" 70 | docker run --rm -d --link theo-mysql-db \ 71 | --name theo-mysql-test \ 72 | -e DB_ENGINE=mysql \ 73 | -e DB_HOST=theo-mysql-db \ 74 | -e DB_USER=${MYSQL_USER} \ 75 | -e DB_PASSWORD=${MYSQL_PASSWORD} \ 76 | -e DB_NAME=${MYSQL_DATABASE} \ 77 | theo:test 78 | docker stop theo-mysql-test 79 | docker run --rm --link theo-mysql-db \ 80 | --name theo-mysql-test \ 81 | -e DB_HOST=theo-mysql-db \ 82 | -e DB_USER=${MYSQL_USER} \ 83 | -e DB_PASSWORD=${MYSQL_PASSWORD} \ 84 | -e DB_NAME=${MYSQL_DATABASE} \ 85 | theo-tester \ 86 | npm run test:upgrade:mysql 87 | RETVAL=$? 88 | if [[ ${RETVAL} -gt 0 ]]; then 89 | echo "ERR mysql upgrade FAILED" 90 | fi 91 | docker stop theo-mysql-db 92 | if [[ ${RETVAL} -gt 0 ]]; then 93 | exit ${RETVAL} 94 | fi 95 | } 96 | 97 | test_mariadb_upgrade() { 98 | # start mariadb 99 | echo "Starting mariadb-server" 100 | docker run --rm --name theo-mariadb-db -d \ 101 | -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \ 102 | -e MYSQL_USER=${MYSQL_USER} \ 103 | -e MYSQL_PASSWORD=${MYSQL_PASSWORD} \ 104 | -e MYSQL_DATABASE=${MYSQL_DATABASE} \ 105 | ${MARIADB_IMAGE} 106 | echo "Wait 60 s.." 107 | sleep 60 108 | # start theo old 109 | echo "Starting ${THEO_INITIAL_IMAGE}" 110 | docker run --rm -d --link theo-mariadb-db --name theo-mariadb-test \ 111 | -e DB_ENGINE=mariadb \ 112 | -e DB_HOST=theo-mariadb-db \ 113 | -e DB_USER=${MYSQL_USER} \ 114 | -e DB_PASSWORD=${MYSQL_PASSWORD} \ 115 | -e DB_NAME=${MYSQL_DATABASE} \ 116 | $THEO_INITIAL_IMAGE 117 | echo "Wait 10 s.." 118 | sleep 10 119 | docker stop theo-mariadb-test 120 | echo "Starting current theo image" 121 | docker run --rm -d --link theo-mariadb-db \ 122 | --name theo-mariadb-test \ 123 | -e DB_ENGINE=mariadb \ 124 | -e DB_HOST=theo-mariadb-db \ 125 | -e DB_USER=${MYSQL_USER} \ 126 | -e DB_PASSWORD=${MYSQL_PASSWORD} \ 127 | -e DB_NAME=${MYSQL_DATABASE} \ 128 | theo:test 129 | docker stop theo-mariadb-test 130 | docker run --rm --link theo-mariadb-db \ 131 | --name theo-mariadb-test \ 132 | -e DB_HOST=theo-mariadb-db \ 133 | -e DB_USER=${MYSQL_USER} \ 134 | -e DB_PASSWORD=${MYSQL_PASSWORD} \ 135 | -e DB_NAME=${MYSQL_DATABASE} \ 136 | theo-tester \ 137 | npm run test:upgrade:mysql 138 | RETVAL=$? 139 | if [[ ${RETVAL} -gt 0 ]]; then 140 | echo "ERR mariadb upgrade FAILED" 141 | fi 142 | docker stop theo-mariadb-db 143 | if [[ ${RETVAL} -gt 0 ]]; then 144 | exit ${RETVAL} 145 | fi 146 | } 147 | 148 | test_sqlite_upgrade 149 | 150 | test_mariadb_upgrade 151 | 152 | test_mysql_upgrade 153 | -------------------------------------------------------------------------------- /scripts/buildDockerImage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | DOCKER_IMAGE=theoapp/theo 6 | 7 | dir_path=$(dirname $0) 8 | DOCKER_TAG=$(${dir_path}/getVersion.sh) 9 | 10 | docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} . 11 | 12 | if [ "$1" = "push" ]; then 13 | docker push ${DOCKER_IMAGE}:${DOCKER_TAG} 14 | fi 15 | 16 | LATEST_TAG=$(git tag | sort --version-sort | tail -n 1) 17 | 18 | if [ "$LATEST_TAG" = "$DOCKER_TAG" ]; then 19 | docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest 20 | if [ "$1" = "push" ]; then 21 | docker push ${DOCKER_IMAGE}:latest 22 | fi 23 | fi -------------------------------------------------------------------------------- /scripts/buildDockerImages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | if [ -z "$DOCKER_IMAGE" ]; then 6 | DOCKER_IMAGE=theoapp/theo 7 | fi 8 | 9 | if [ -z "$DOCKER_TAG" ]; then 10 | DOCKER_TAG=$(cat package.json \ 11 | | grep version \ 12 | | head -1 \ 13 | | awk -F: '{ print $2 }' \ 14 | | sed 's/[",]//g' \ 15 | | tr -d '[[:space:]]') 16 | fi 17 | 18 | is_valid_tag () { 19 | if [[ "$DOCKER_TAG" =~ ^[0-9]*\.[0-9]*\.[0-9]* ]]; then 20 | return 0 21 | else 22 | return 1 23 | fi 24 | } 25 | 26 | is_latest_tag () { 27 | LATEST_GIT_TAG=$(git tag | grep -E '^[0-9]*\.[0-9]*\.[0-9]*$' | sort -V | tail -n 1) 28 | if [ "$LATEST_GIT_TAG" = "$DOCKER_TAG" ]; then 29 | return 0 30 | else 31 | return 1 32 | fi 33 | } 34 | 35 | docker_build () { 36 | docker build -t ${DOCKER_IMAGE}:"${1}" . 37 | } 38 | 39 | docker_push () { 40 | is_valid_tag 41 | RETVAL=$? 42 | if [ $RETVAL -eq 0 ]; then 43 | docker push ${DOCKER_IMAGE}:"${1}" 44 | else 45 | echo "Don't push. Invalid tag" 46 | fi 47 | } 48 | 49 | build_full () { 50 | TAG_NAME=${DOCKER_TAG} 51 | 52 | docker_build "${TAG_NAME}" 53 | 54 | if [ "$1" = "push" ]; then 55 | docker_push "${TAG_NAME}" 56 | fi 57 | 58 | is_latest_tag 59 | RETVAL=$? 60 | if [ $RETVAL -eq 0 ]; then 61 | docker tag ${DOCKER_IMAGE}:${TAG_NAME} ${DOCKER_IMAGE}:latest 62 | if [ "$1" = "push" ]; then 63 | docker push ${DOCKER_IMAGE}:latest 64 | fi 65 | fi 66 | } 67 | 68 | build_sqlite () { 69 | TAG_NAME=${DOCKER_TAG}-sqlite 70 | 71 | npm uninstall @authkeys/mysql-connman redis memcached 72 | docker_build "${TAG_NAME}" 73 | 74 | if [ "$1" = "push" ]; then 75 | docker_push "${TAG_NAME}" 76 | fi 77 | 78 | git checkout package.json package-lock.json 79 | 80 | } 81 | 82 | build_sqlite_redis () { 83 | TAG_NAME=${DOCKER_TAG}-sqlite-redis 84 | 85 | npm uninstall @authkeys/mysql-connman memcached 86 | docker_build "${TAG_NAME}" 87 | 88 | if [ "$1" = "push" ]; then 89 | docker_push "${TAG_NAME}" 90 | fi 91 | 92 | git checkout package.json package-lock.json 93 | } 94 | 95 | build_sqlite_memcached () { 96 | 97 | TAG_NAME=${DOCKER_TAG}-sqlite-memcached 98 | 99 | npm uninstall @authkeys/mysql-connman redis 100 | docker_build "${TAG_NAME}" 101 | 102 | if [ "$1" = "push" ]; then 103 | docker_push "${TAG_NAME}" 104 | fi 105 | 106 | git checkout package.json package-lock.json 107 | 108 | } 109 | 110 | build_mysql () { 111 | 112 | TAG_NAME=${DOCKER_TAG}-mysql 113 | 114 | npm uninstall sqlite3 redis memcached 115 | docker_build "${TAG_NAME}" 116 | 117 | if [ "$1" = "push" ]; then 118 | docker_push "${TAG_NAME}" 119 | fi 120 | 121 | git checkout package.json package-lock.json 122 | 123 | } 124 | 125 | build_mysql_redis () { 126 | TAG_NAME=${DOCKER_TAG}-mysql-redis 127 | 128 | npm uninstall sqlite3 memcached 129 | docker_build "${TAG_NAME}" 130 | 131 | if [ "$1" = "push" ]; then 132 | docker_push "${TAG_NAME}" 133 | fi 134 | 135 | git checkout package.json package-lock.json 136 | 137 | } 138 | 139 | build_mysql_memcached () { 140 | TAG_NAME=${DOCKER_TAG}-mysql-memcached 141 | 142 | npm uninstall sqlite3 redis 143 | docker_build "${TAG_NAME}" 144 | 145 | if [ "$1" = "push" ]; then 146 | docker_push "${TAG_NAME}" 147 | fi 148 | 149 | git checkout package.json package-lock.json 150 | 151 | } 152 | 153 | if [ "$1" = "all" ]; then 154 | 155 | build_sqlite "$2" 156 | build_sqlite_redis "$2" 157 | build_sqlite_memcached "$2" 158 | 159 | build_mysql "$2" 160 | build_mysql_redis "$2" 161 | build_mysql_memcached "$2" 162 | 163 | build_full "$2" 164 | 165 | else 166 | build_${1} "$2" 167 | fi 168 | -------------------------------------------------------------------------------- /scripts/getVersion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dir_path=$(dirname $0) 4 | cat ${dir_path}/../package.json \ 5 | | grep version \ 6 | | head -1 \ 7 | | awk -F: '{ print $2 }' \ 8 | | sed 's/[",]//g' \ 9 | | tr -d '[[:space:]]' -------------------------------------------------------------------------------- /scripts/updateVersion.sh: -------------------------------------------------------------------------------- 1 | 2 | dir_path=$(dirname $0) 3 | PACKAGE_VERSION=$(${dir_path}/getVersion.sh) 4 | 5 | sed -i '' 's/org.opencontainers.image.version=.*/org.opencontainers.image.version="'"${PACKAGE_VERSION}"'" \\/' Dockerfile 6 | 7 | git add Dockerfile 8 | -------------------------------------------------------------------------------- /settings.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "admin": { 3 | "token": "TMP_PASSWORD" 4 | }, 5 | "client": { 6 | "tokens": [ 7 | "njknsjd2412fnjkasnj", 8 | "knkjnknfjfnjenkln" 9 | ] 10 | }, 11 | "db": { 12 | "engine": "sqlite", 13 | "storage": "./data/theo.db" 14 | }, 15 | "server": { 16 | "http_port": 8890 17 | } 18 | } -------------------------------------------------------------------------------- /src/appenv.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import dotenv from 'dotenv'; 16 | 17 | dotenv.config(); 18 | -------------------------------------------------------------------------------- /src/cacheInitiator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import CacheHelper from './lib/helpers/CacheHelper'; 16 | import { common_error, common_info, common_warn } from './lib/utils/logUtils'; 17 | import RedisManager from './lib/cache/redis'; 18 | 19 | const MAX_RETRY_TIMEOUT = 5000; 20 | let retriesCount = 0; 21 | 22 | const redisSubscribe = (cm, dm, ah) => { 23 | const conn = cm.getClient(); 24 | conn.on('error', e => { 25 | retriesCount++; 26 | const nextRetry = Math.min(retriesCount * retriesCount * 50, MAX_RETRY_TIMEOUT); 27 | common_error('%s. Retrying in %d ms', e.message, nextRetry); 28 | // Close connection and try again... 29 | cm.close(conn); 30 | setTimeout(function () { 31 | redisSubscribe(cm, dm, ah); 32 | }, nextRetry); 33 | }); 34 | 35 | conn.on('ready', () => { 36 | common_info('Connected to redis attempt #', retriesCount); 37 | conn.subscribe('core_tokens'); 38 | retriesCount = 0; 39 | }); 40 | 41 | conn.on('message', async (channel, message) => { 42 | if (channel === 'core_tokens' && message === 'flush_tokens') { 43 | common_warn('Flushing tokens!'); 44 | let dbClient; 45 | try { 46 | dbClient = dm.getClient(); 47 | await dbClient.open(); 48 | await ah.loadAuthTokens(dbClient); 49 | } catch (e) { 50 | common_error('Failed to reload tokens!!!', e.message); 51 | } finally { 52 | if (dbClient) { 53 | dbClient.close(); 54 | } 55 | } 56 | } 57 | }); 58 | }; 59 | 60 | const initCache = function (subscribe_core_token, dm, ah) { 61 | let ch; 62 | let cm; 63 | try { 64 | ch = CacheHelper(ah.getSettings('cache')); 65 | } catch (err) { 66 | common_error('Unable to create CacheHelper %s', err.message); 67 | } 68 | if (ch) { 69 | cm = ch.getManager(); 70 | if (cm) { 71 | if (!subscribe_core_token) { 72 | common_info('Flushing CacheManager'); 73 | cm.flush().catch(er => { 74 | common_error('Failed to initialize CacheManager: %s', er.message); 75 | }); 76 | } else { 77 | if (cm instanceof RedisManager) { 78 | redisSubscribe(cm, dm, ah); 79 | } 80 | } 81 | } 82 | } 83 | return cm; 84 | }; 85 | 86 | export default initCache; 87 | -------------------------------------------------------------------------------- /src/dbInitiator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | let loadDbEnvSettings; 16 | try { 17 | loadDbEnvSettings = require('@authkeys/mysql-connman').loadDbEnvSettings; 18 | } catch (e) { 19 | loadDbEnvSettings = function (settings) { 20 | throw new Error( 21 | 'loadDbEnvSettings not loaded. Are you using a specialized version of theo-node without mysql support?' 22 | ); 23 | }; 24 | } 25 | export const getLoadDbEnvSettings = function () { 26 | return loadDbEnvSettings; 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/cache/inmemory/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import CachedManager from '../../managers/CacheManager'; 16 | 17 | const cache = {}; 18 | 19 | class InMemoryCacheManager extends CachedManager { 20 | constructor(settings) { 21 | super(settings.options); 22 | } 23 | 24 | set(key, value) { 25 | return new Promise(resolve => { 26 | cache[key] = value; 27 | resolve(true); 28 | }); 29 | } 30 | 31 | get(key) { 32 | return new Promise(resolve => { 33 | resolve(cache[key]); 34 | }); 35 | } 36 | 37 | del(key) { 38 | return new Promise(resolve => { 39 | if (key in cache) { 40 | delete cache[key]; 41 | resolve(true); 42 | } else { 43 | resolve(false); 44 | } 45 | }); 46 | } 47 | 48 | flush() { 49 | return new Promise((resolve, reject) => { 50 | Object.keys(cache).forEach(k => { 51 | delete cache[k]; 52 | }); 53 | resolve(true); 54 | }); 55 | } 56 | 57 | quit() {} 58 | } 59 | 60 | export default InMemoryCacheManager; 61 | -------------------------------------------------------------------------------- /src/lib/cache/memcached/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import CachedManager from '../../managers/CacheManager'; 16 | 17 | const MAX_EXPIRATION = 2592000; 18 | 19 | class MemcachedManager extends CachedManager { 20 | constructor(settings) { 21 | super(settings); 22 | const options = { 23 | failures: 1, 24 | timeout: 1000, 25 | retries: 1, 26 | retry: 1000 27 | }; 28 | try { 29 | const Memcached = require('memcached'); 30 | this.memcached = new Memcached(settings.uri, options); 31 | console.log('MemcachedManager started'); 32 | } catch (e) { 33 | // Module not loaded 34 | } 35 | } 36 | 37 | set(key, value) { 38 | return new Promise((resolve, reject) => { 39 | this.memcached.set(key, value, MAX_EXPIRATION, err => { 40 | if (err) { 41 | return reject(err); 42 | } 43 | resolve(true); 44 | }); 45 | }); 46 | } 47 | 48 | get(key) { 49 | return new Promise((resolve, reject) => { 50 | this.memcached.get(key, (err, data) => { 51 | if (err) { 52 | return reject(err); 53 | } 54 | if (data === undefined) { 55 | data = null; 56 | } 57 | resolve(data); 58 | }); 59 | }); 60 | } 61 | 62 | del(key) { 63 | return new Promise((resolve, reject) => { 64 | this.memcached.delete(key, err => { 65 | if (err) { 66 | return reject(err); 67 | } 68 | resolve(true); 69 | }); 70 | }); 71 | } 72 | 73 | flush() { 74 | return new Promise((resolve, reject) => { 75 | this.memcached.flush(err => { 76 | if (err) { 77 | return reject(err); 78 | } 79 | resolve(true); 80 | }); 81 | }); 82 | } 83 | 84 | quit() { 85 | try { 86 | this.memcached.end(); 87 | } catch (e) {} 88 | } 89 | } 90 | 91 | export default MemcachedManager; 92 | -------------------------------------------------------------------------------- /src/lib/cache/modules.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Memcached from './memcached'; 16 | import Redis from './redis'; 17 | import InMemoryCacheManager from './inmemory'; 18 | 19 | const modules = { 20 | memcached: Memcached, 21 | redis: Redis, 22 | inmemory: InMemoryCacheManager 23 | }; 24 | 25 | export const getCacheModule = name => { 26 | return modules[name]; 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/cache/redis/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import CachedManager from '../../managers/CacheManager'; 16 | import { common_error, common_info } from '../../utils/logUtils'; 17 | 18 | let redis; 19 | try { 20 | redis = require('redis'); 21 | } catch (e) { 22 | // not loaded 23 | } 24 | 25 | class RedisManager extends CachedManager { 26 | constructor(settings) { 27 | super(settings.options); 28 | if (!redis) { 29 | throw new Error('module redis not installed!'); 30 | } 31 | // parse options.. 32 | const _options = {}; 33 | if (settings.options) { 34 | const parts = settings.options.split(','); 35 | parts.forEach(part => { 36 | const keyval = part.split('='); 37 | _options[keyval[0]] = keyval[1]; 38 | }); 39 | } 40 | this.options = { 41 | url: settings.uri, 42 | password: _options.password 43 | }; 44 | this.testConn().then().catch(); 45 | } 46 | 47 | async testConn() { 48 | try { 49 | const conn = await this.open(); 50 | common_info('Redis server: ', conn.server_info.redis_version); 51 | this.close(conn); 52 | } catch (e) { 53 | common_error('Failed to test redis connection', e); 54 | } 55 | } 56 | 57 | set(key, value) { 58 | return new Promise((resolve, reject) => { 59 | this.open() 60 | .then(redis => { 61 | redis.set(key, value, err => { 62 | this.close(redis); 63 | if (err) { 64 | return reject(err); 65 | } 66 | resolve(true); 67 | }); 68 | }) 69 | .catch(e => { 70 | reject(e); 71 | }); 72 | }); 73 | } 74 | 75 | get(key) { 76 | return new Promise((resolve, reject) => { 77 | this.open() 78 | .then(redis => { 79 | redis.get(key, (err, data) => { 80 | this.close(redis); 81 | if (err) { 82 | return reject(err); 83 | } 84 | resolve(data); 85 | }); 86 | }) 87 | .catch(e => { 88 | reject(e); 89 | }); 90 | }); 91 | } 92 | 93 | del(key) { 94 | return new Promise((resolve, reject) => { 95 | this.open() 96 | .then(redis => { 97 | redis.del(key, err => { 98 | this.close(redis); 99 | if (err) { 100 | return reject(err); 101 | } 102 | resolve(true); 103 | }); 104 | }) 105 | .catch(e => { 106 | reject(e); 107 | }); 108 | }); 109 | } 110 | 111 | flush() { 112 | return new Promise((resolve, reject) => { 113 | this.open() 114 | .then(redis => { 115 | redis.flushdb(err => { 116 | this.close(redis); 117 | if (err) { 118 | return reject(err); 119 | } 120 | resolve(true); 121 | }); 122 | }) 123 | .catch(e => { 124 | reject(e); 125 | }); 126 | }); 127 | } 128 | 129 | close(conn) { 130 | try { 131 | conn.quit(); 132 | } catch (e) { 133 | common_error(e.message); 134 | } 135 | } 136 | 137 | open() { 138 | return new Promise((resolve, reject) => { 139 | const conn = redis.createClient(this.options); 140 | conn.on('error', e => { 141 | reject(e); 142 | }); 143 | conn.on('ready', () => { 144 | resolve(conn); 145 | }); 146 | }); 147 | } 148 | 149 | getClient() { 150 | return redis.createClient(this.options); 151 | } 152 | } 153 | 154 | export default RedisManager; 155 | -------------------------------------------------------------------------------- /src/lib/helpers/AdminHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export * from './admins/AdminAccountHelper'; 16 | export * from './admins/AdminKeyHelper'; 17 | export * from './admins/AdminPermissionHelper'; 18 | export * from './admins/AdminGroupHelper'; 19 | export * from './admins/AdminGroupAccountHelper'; 20 | export * from './admins/AdminGroupPermissionHelper'; 21 | -------------------------------------------------------------------------------- /src/lib/helpers/AppHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import AuthTokenManager from '../managers/AuthTokenManager'; 16 | 17 | let _instance; 18 | 19 | class AppHelper { 20 | constructor(settings) { 21 | this.settings = settings; 22 | } 23 | 24 | getSettings(root) { 25 | return root ? this.settings[root] : this.settings; 26 | } 27 | 28 | async loadAuthTokens(db) { 29 | const atm = new AuthTokenManager(db); 30 | const tokens = await atm.getAll(); 31 | this.reloadAuthToken(tokens); 32 | return true; 33 | } 34 | 35 | reloadAuthToken(tokens) { 36 | this.settings.admin.token = undefined; 37 | this.settings.admin.tokens = tokens.admins || {}; 38 | if (tokens.admin) { 39 | const { token, assignee } = AuthTokenManager.getTokenAssignee(tokens.admin, true); 40 | this.settings.admin.tokens[token] = assignee; 41 | } 42 | this.settings.client.tokens = tokens.clients; 43 | } 44 | } 45 | 46 | const getInstance = settings => { 47 | if (!_instance) { 48 | if (!settings) { 49 | throw new Error('AppHelper needs to be initialized with settings'); 50 | } 51 | _instance = new AppHelper(settings); 52 | } else { 53 | if (settings) { 54 | _instance.settings = settings; 55 | } 56 | } 57 | return _instance; 58 | }; 59 | 60 | export default getInstance; 61 | -------------------------------------------------------------------------------- /src/lib/helpers/AuditHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import packageJson from '../../../package'; 16 | import { http_post } from '../utils/httpUtils'; 17 | import { common_error, common_info } from '../utils/logUtils'; 18 | 19 | const { LOG_AUDIT_CONSOLE, LOG_AUDIT_URL, LOG_AUDIT_TOKEN } = process.env; 20 | 21 | const auditEnable = (LOG_AUDIT_CONSOLE && LOG_AUDIT_CONSOLE === 'true') || LOG_AUDIT_URL; 22 | 23 | const getHTTPHeaders = function () { 24 | if (!LOG_AUDIT_URL) return; 25 | const headers = { 26 | 'User-Agent': packageJson.name + '/' + packageJson.version, 27 | 'Content-type': 'application/json' 28 | }; 29 | if (LOG_AUDIT_TOKEN) { 30 | headers.Authorization = 'Bearer ' + LOG_AUDIT_TOKEN; 31 | } 32 | return headers; 33 | }; 34 | 35 | const auditHTTPHeaders = getHTTPHeaders(); 36 | 37 | class AuditHelper { 38 | constructor(req) { 39 | this.req = req; 40 | } 41 | 42 | log(context, action, entity, data) { 43 | if (!auditEnable) { 44 | return; 45 | } 46 | const source_ip = this.req.headers['x-forwarded-for'] 47 | ? this.req.headers['x-forwarded-for'].split(',')[0].trim() 48 | : this.req.connection.remoteAddress; 49 | const user_agent = this.req.headers['user-agent'] || 'unknown'; 50 | const { auth_token: author } = this.req; 51 | const obj = { 52 | ts: new Date().getTime(), 53 | source_ip, 54 | user_agent, 55 | author, 56 | context, 57 | action, 58 | entity, 59 | data 60 | }; 61 | if (LOG_AUDIT_CONSOLE && LOG_AUDIT_CONSOLE === 'true') { 62 | setImmediate(() => { 63 | common_info('[AUDIT]', JSON.stringify(obj)); 64 | }); 65 | return; 66 | } 67 | if (LOG_AUDIT_URL) { 68 | setImmediate(() => { 69 | http_post(LOG_AUDIT_URL, obj, auditHTTPHeaders).catch(e => { 70 | common_error('Unable to send audit log', JSON.stringify(obj), e.message); 71 | }); 72 | }); 73 | } 74 | } 75 | } 76 | 77 | export default AuditHelper; 78 | -------------------------------------------------------------------------------- /src/lib/helpers/CacheHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { getCacheModule } from '../cache/modules'; 16 | import EventHelper from './EventHelper'; 17 | let _instance; 18 | 19 | class CacheHelper { 20 | constructor(settings) { 21 | if (!settings) { 22 | return; 23 | } 24 | if (settings.type && settings.type !== 'false') { 25 | const ManagerClass = getCacheModule(settings.type); 26 | if (!ManagerClass) { 27 | throw new Error('Invalid cache module ' + settings.type); 28 | } 29 | this.manager = new ManagerClass(settings.settings); 30 | } 31 | EventHelper.on('theo:flushdb', () => { 32 | setImmediate(() => { 33 | if (this.manager) { 34 | this.manager.flush().catch(console.error); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | getManager() { 41 | return this.manager; 42 | } 43 | } 44 | 45 | const getInstance = settings => { 46 | if (!_instance) { 47 | _instance = new CacheHelper(settings); 48 | } 49 | return _instance; 50 | }; 51 | 52 | export const loadCacheManager = function () { 53 | const ch = getInstance(); 54 | const cm = ch.getManager(); 55 | if (!cm) { 56 | return false; 57 | } 58 | return cm; 59 | }; 60 | 61 | export default getInstance; 62 | -------------------------------------------------------------------------------- /src/lib/helpers/DbHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import EventHelper from './EventHelper'; 16 | import { getRdbmsModule } from '../rdbms/modules'; 17 | import { common_debug, common_error, common_info } from '../utils/logUtils'; 18 | import { setTimeoutPromise } from '../utils/processUtils'; 19 | 20 | let _instance; 21 | 22 | class DbHelper { 23 | settings; 24 | 25 | manager; 26 | 27 | constructor(settings) { 28 | if (!settings) { 29 | throw new Error('DbHelper() needs settings.db'); 30 | } 31 | if (!settings.engine) { 32 | throw new Error('DbHelper() needs settings.db.engine'); 33 | } 34 | this.settings = settings; 35 | } 36 | 37 | getManager() { 38 | if (!this.manager) { 39 | const ManagerClass = getRdbmsModule(this.settings.engine); 40 | if (!ManagerClass) { 41 | throw new Error('Invalid rdbms module ' + this.settings.engine); 42 | } 43 | this.manager = new ManagerClass(this.settings); 44 | } 45 | return this.manager; 46 | } 47 | 48 | async init(client) { 49 | const gotClient = !!client; 50 | try { 51 | if (!client) { 52 | client = this.manager.getClient(); 53 | await client.open(); 54 | } 55 | await this.checkDb(client); 56 | } finally { 57 | if (!gotClient && client) { 58 | try { 59 | await client.close(); 60 | } catch (e) { 61 | // 62 | } 63 | } 64 | } 65 | } 66 | 67 | async checkDb(client) { 68 | let currentVersion; 69 | try { 70 | const serverVersion = await client.getServerVersion(); 71 | common_info('Db Version', serverVersion); 72 | this.manager.setClient(client); 73 | } catch (e) { 74 | common_error('checkDb failed %s', e.message); 75 | process.exit(99); 76 | } 77 | try { 78 | const row = await this.manager.getCurrentVersion(); 79 | if (row === false) { 80 | currentVersion = -999; 81 | } else if (row) { 82 | currentVersion = row.value; 83 | } 84 | } catch (e) { 85 | common_error('Failed to read current version, exiting. ', e.message); 86 | process.exit(92); 87 | } 88 | 89 | common_info('Check db: currentVersion %s targetVersion %s', currentVersion, this.manager.dbVersion); 90 | 91 | if (currentVersion === -999) { 92 | try { 93 | currentVersion = await this.manager.createVersionTable(); 94 | } catch (e) { 95 | if (e.code === 'ER_TABLE_EXISTS_ERROR') { 96 | await setTimeoutPromise(2000); 97 | return this.checkDb(client); 98 | } else { 99 | common_error('Unable to create _version table: [%s] %s', e.code, e.message); 100 | process.exit(90); 101 | } 102 | } 103 | } else if (currentVersion < 0) { 104 | common_error('Db in initialization, exiting '); 105 | process.exit(91); 106 | } 107 | 108 | try { 109 | if (currentVersion === 0) { 110 | await this.manager.initDb(); 111 | common_info('Db created'); 112 | return true; 113 | } 114 | if (this.manager.dbVersion === currentVersion) { 115 | return true; 116 | } 117 | if (this.manager.dbVersion > currentVersion) { 118 | await this.manager.upgradeDb(currentVersion); 119 | common_info('Db updated to ', this.manager.dbVersion); 120 | return true; 121 | } 122 | } catch (e) { 123 | common_error('\n'); 124 | common_error('\n'); 125 | common_error(' !!!!!!!! FATAL ERROR !!!!!!!!'); 126 | common_error('\n'); 127 | common_error('checkDb failed %s', e.message); 128 | common_error('\n'); 129 | common_error(' !!!!!!!! FATAL ERROR !!!!!!!!'); 130 | common_error('\n'); 131 | common_error('\n'); 132 | console.error(e); 133 | process.exit(96); 134 | } 135 | return true; 136 | } 137 | 138 | async _flush(client) { 139 | const done = await this.manager.flushDb(); 140 | if (done) { 141 | EventHelper.emit('theo:flushdb'); 142 | try { 143 | await this.checkDb(client); 144 | common_debug('check db done'); 145 | return true; 146 | } catch (e) { 147 | common_error('Failed to check db', e); 148 | throw e; 149 | } 150 | } else { 151 | throw new Error('Unable to flush db'); 152 | } 153 | } 154 | 155 | close() { 156 | return this.manager.close(); 157 | } 158 | } 159 | 160 | const getInstance = settings => { 161 | if (!_instance) { 162 | _instance = new DbHelper(settings); 163 | } 164 | return _instance; 165 | }; 166 | 167 | export const releaseDHInstance = async () => { 168 | if (_instance !== null) { 169 | await _instance.close(); 170 | } 171 | }; 172 | 173 | export default getInstance; 174 | -------------------------------------------------------------------------------- /src/lib/helpers/EventHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import EventEmitter from 'events'; 16 | 17 | const eventEmitter = new EventEmitter(); 18 | 19 | export default eventEmitter; 20 | -------------------------------------------------------------------------------- /src/lib/helpers/ImpExpHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import GroupManager from '../managers/GroupManager'; 16 | import AccountManager from '../managers/AccountManager'; 17 | import { MAX_ROWS } from '../managers/BaseCacheManager'; 18 | import PermissionManager from '../managers/PermissionManager'; 19 | import KeyManager from '../managers/KeyManager'; 20 | import GroupAccountManager from '../managers/GroupAccountManager'; 21 | 22 | const getAllGroups = async db => { 23 | const gm = new GroupManager(db); 24 | const gam = new GroupAccountManager(db); 25 | const pm = new PermissionManager(db); 26 | const total = await gm.getAllCount('where is_internal = ? ', [0]); 27 | const items = []; 28 | while (items.length < total) { 29 | const itemIds = await gm.getAll(MAX_ROWS, items.length, false, 'id', 'asc'); 30 | for (let i = 0; i < itemIds.rows.length; i++) { 31 | const account = await gm.getFull(itemIds.rows[i].id, gam, pm); 32 | items.push(account); 33 | } 34 | } 35 | return items; 36 | }; 37 | 38 | const getAllAccounts = async db => { 39 | const am = new AccountManager(db); 40 | const total = await am.getAllCount(); 41 | const items = []; 42 | while (items.length < total) { 43 | const itemIds = await am.getAll(MAX_ROWS, items.length, false, 'id', 'asc'); 44 | for (let i = 0; i < itemIds.rows.length; i++) { 45 | const account = await am.getFull(itemIds.rows[i].id); 46 | items.push(account); 47 | } 48 | } 49 | return items; 50 | }; 51 | 52 | export const exp = async db => { 53 | const groups = await getAllGroups(db); 54 | const accounts = await getAllAccounts(db); 55 | return { 56 | accounts, 57 | groups 58 | }; 59 | }; 60 | 61 | export const imp = async (db, dump) => { 62 | const pm = new PermissionManager(db); 63 | const gm = new GroupManager(db); 64 | if (dump.groups) { 65 | for (let i = 0; i < dump.groups.length; i++) { 66 | const group = dump.groups[i]; 67 | try { 68 | const id = await gm.create(group.name, group.active); 69 | for (let ii = 0; ii < group.permissions.length; ii++) { 70 | const permission = group.permissions[ii]; 71 | await pm.create(id, permission.user, permission.host, permission.ssh_options); 72 | } 73 | } catch (e) { 74 | console.error('IMP: Failed to create group %s', group.name, e); 75 | } 76 | } 77 | } 78 | if (dump.accounts) { 79 | const am = new AccountManager(db); 80 | const km = new KeyManager(db, am); 81 | const gam = new GroupAccountManager(db); 82 | for (let i = 0; i < dump.accounts.length; i++) { 83 | const account = dump.accounts[i]; 84 | try { 85 | const id = await am.create(account); 86 | for (let ii = 0; ii < account.public_keys.length; ii++) { 87 | const public_key = account.public_keys[ii]; 88 | if (!public_key.fingerprint) { 89 | // TODO get fingerprint! 90 | } 91 | await km.create(id, public_key.public_key, public_key.fingerprint, public_key.public_key_sig); 92 | } 93 | const group_id = await gm.create(account.email); 94 | for (let ii = 0; ii < account.groups.length; ii++) { 95 | const group = account.groups[ii]; 96 | const group_id = await gm.getIdByName(group.name); 97 | await gam.create(group_id, id); 98 | } 99 | for (let ii = 0; ii < account.permissions.length; ii++) { 100 | const permission = account.permissions[ii]; 101 | if (permission.is_internal) { 102 | await pm.create(group_id, permission.user, permission.host, permission.ssh_options); 103 | } 104 | } 105 | } catch (e) { 106 | console.error('IMP: Failed to create account %s', account.email, e); 107 | } 108 | } 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/lib/helpers/KeysHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import PermissionManager from '../managers/PermissionManager'; 16 | import { loadCacheManager } from './CacheHelper'; 17 | 18 | let _cm; 19 | 20 | const checkCache = async key => { 21 | if (_cm === undefined) { 22 | _cm = loadCacheManager(); 23 | } 24 | if (_cm !== false) { 25 | try { 26 | const keys = await _cm.get(key); 27 | if (keys !== null) { 28 | return keys; 29 | } 30 | } catch (err) { 31 | console.error('Failed to fetch from cache, using db', err.message); 32 | } 33 | } 34 | return false; 35 | }; 36 | 37 | export const getAuthorizedKeys = async (dm, user, host) => { 38 | const cache_key = `${user}_${host}`; 39 | const _cache = await checkCache(cache_key); 40 | if (_cache) return { keys: _cache, cache: true }; 41 | const { keys, cache } = await getAuthorizedKeysAsJson(dm, user, host, true); 42 | const skeys = keys 43 | .filter(key => { 44 | return key !== undefined; 45 | }) 46 | .map(key => { 47 | return `${key.ssh_options ? `${key.ssh_options} ` : ''}${key.public_key}`; 48 | }) 49 | .join('\n'); 50 | if (_cm !== false) { 51 | _cm.set(cache_key, skeys).catch(err => console.error('Failed to save cache for %s', cache_key, err)); 52 | } 53 | return { keys: skeys, cache }; 54 | }; 55 | 56 | export const getAuthorizedKeysAsJson = async (dm, user, host, skip_cache = false) => { 57 | const cache_key = `json:${user}_${host}`; 58 | if (!skip_cache) { 59 | const cache = await checkCache(cache_key); 60 | if (cache) { 61 | return { keys: JSON.parse(cache), cache: true }; 62 | } 63 | } 64 | const db = dm.getClient('ro'); 65 | await db.open(); 66 | const pm = new PermissionManager(db); 67 | let keys; 68 | try { 69 | keys = await pm.match(user, host); 70 | } catch (err) { 71 | err.t_code = 500; 72 | throw err; 73 | } finally { 74 | const ret = db.close(); 75 | if (ret && ret instanceof Promise) { 76 | ret.catch(err => console.error(err)); 77 | } 78 | } 79 | if (!skip_cache) { 80 | if (_cm !== false) { 81 | setImmediate(() => { 82 | _cm 83 | .set(cache_key, JSON.stringify(keys)) 84 | .catch(err => console.error('Failed to save cache for %s', cache_key, err)); 85 | }); 86 | } 87 | } 88 | return { keys, cache: false }; 89 | }; 90 | 91 | export const getAuthorizedKeysAsFullJson = async (db, user, host) => { 92 | const pm = new PermissionManager(db); 93 | let keys; 94 | try { 95 | keys = await pm.search(user, host); 96 | } catch (err) { 97 | err.t_code = 500; 98 | throw err; 99 | } 100 | return keys; 101 | }; 102 | -------------------------------------------------------------------------------- /src/lib/helpers/PluginHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import path from 'path'; 16 | import { getDirFiles } from '../utils/fsUtils'; 17 | import EventHelper from './EventHelper'; 18 | 19 | const PLUGIN_PATH = 'plugins'; 20 | 21 | const loadPlugin = async name => { 22 | const plugin = require(path.join(path.resolve(PLUGIN_PATH), name)); 23 | if (plugin) { 24 | plugin.init(EventHelper); 25 | } 26 | }; 27 | 28 | export const loadPlugins = async plugins => { 29 | const _plugins = plugins.split(','); 30 | try { 31 | const files = await getDirFiles(PLUGIN_PATH); 32 | if (files) { 33 | for (let i = 0; i < files.length; i++) { 34 | if (_plugins.indexOf(files[i]) >= 0) { 35 | try { 36 | await loadPlugin(files[i]); 37 | } catch (e) { 38 | console.error('[ %s ] Failed to load plugin %s:', new Date().toISOString(), files[i], e.message); 39 | } 40 | } 41 | } 42 | } 43 | } catch (err) { 44 | console.error('[ %s ] Unable to initialize plugins', new Date().toISOString(), err.message); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/helpers/RemoteLoggerHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import packageJson from '../../../package'; 16 | import { http_post } from '../utils/httpUtils'; 17 | import { common_error } from '../utils/logUtils'; 18 | 19 | const { LOG_AUTH_KEYS_URL, LOG_AUTH_KEYS_TOKEN } = process.env; 20 | 21 | class RemoteLoggerHelper { 22 | static getHttpHeaders() { 23 | return { 24 | 'User-Agent': packageJson.name + '/' + packageJson.version, 25 | Authorization: 'Bearer ' + LOG_AUTH_KEYS_TOKEN, 26 | 'Content-type': 'application/json' 27 | }; 28 | } 29 | 30 | static log(data) { 31 | if (!LOG_AUTH_KEYS_URL) return; 32 | setImmediate(() => { 33 | http_post(LOG_AUTH_KEYS_URL, data, this.getHttpHeaders()).catch(e => { 34 | common_error('Failed to POST log: %s', e.message, data); 35 | }); 36 | }); 37 | } 38 | } 39 | 40 | export default RemoteLoggerHelper; 41 | -------------------------------------------------------------------------------- /src/lib/helpers/UpdateHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import packageJson from '../../../package'; 16 | import { http_get } from '../utils/httpUtils'; 17 | import semver from 'semver'; 18 | import { common_debug, common_warn } from '../utils/logUtils'; 19 | 20 | const CHECK_UPDATE_URL = 'https://update.theo.authkeys.io/version'; 21 | 22 | class UpdateHelper { 23 | static _getHttpHeaders() { 24 | return { 25 | 'User-Agent': packageJson.name + '/' + packageJson.version, 26 | Accept: 'application/json' 27 | }; 28 | } 29 | 30 | static checkUpdate = function (quick = false) { 31 | const timeout = quick ? 0 : Math.floor(Math.random() * (60000 - 1000) + 1000); 32 | if (!quick) { 33 | common_debug('checkUpdate in %s ms', timeout); 34 | } 35 | setTimeout(function () { 36 | http_get(CHECK_UPDATE_URL, UpdateHelper._getHttpHeaders()) 37 | .then(data => { 38 | if (!data) { 39 | common_warn('Failed to check new version, empty response'); 40 | return; 41 | } 42 | let version; 43 | let securityUpdate = false; 44 | if (typeof data === 'object') { 45 | version = data.version; 46 | securityUpdate = data.security_update; 47 | } else { 48 | version = data; 49 | } 50 | if (semver.gt(version, packageJson.version)) { 51 | common_warn(''); 52 | common_warn(' !!! %s Update available: %s !!!', securityUpdate ? 'Security' : '', version); 53 | common_warn(''); 54 | } 55 | }) 56 | .catch(e => { 57 | common_warn('Failed to check new version:', e.message); 58 | }); 59 | }, timeout); 60 | }; 61 | } 62 | 63 | export default UpdateHelper; 64 | -------------------------------------------------------------------------------- /src/lib/helpers/admins/AdminGroupHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import GroupManager from '../../managers/GroupManager'; 16 | import PermissionManager from '../../managers/PermissionManager'; 17 | import GroupAccountManager from '../../managers/GroupAccountManager'; 18 | import EventHelper from '../EventHelper'; 19 | 20 | export const adminCreateGroup = async (db, group, req, onlyId = false) => { 21 | if (!group.name) { 22 | const error = new Error('Malformed object, name is required'); 23 | error.t_code = 400; 24 | throw error; 25 | } 26 | 27 | const gm = new GroupManager(db); 28 | 29 | let check; 30 | try { 31 | check = await gm.checkName(group.name); 32 | } catch (err) { 33 | err.t_code = 500; 34 | throw err; 35 | } 36 | if (check) { 37 | const error = new Error('Group already exists'); 38 | error.t_code = 409; 39 | throw error; 40 | } 41 | try { 42 | const id = await gm.create(group.name); 43 | EventHelper.emit('theo:change', { 44 | func: 'group', 45 | action: 'add', 46 | object: id, 47 | receiver: 'admin' 48 | }); 49 | if (req && req.auditHelper) { 50 | if (group.name.indexOf('@') < 0) { 51 | req.auditHelper.log('groups', 'create', group.name); 52 | } 53 | } 54 | if (onlyId) return id; 55 | const pm = new PermissionManager(db, this); 56 | const gam = new GroupAccountManager(db, this); 57 | return gm.getFull(id, gam, pm); 58 | } catch (err) { 59 | err.t_code = 500; 60 | throw err; 61 | } 62 | }; 63 | 64 | export const adminGetGroup = async (db, id) => { 65 | const gm = new GroupManager(db); 66 | const pm = new PermissionManager(db, this); 67 | const gam = new GroupAccountManager(db, this); 68 | try { 69 | if (isNaN(id)) { 70 | id = await gm.getIdByName(id); 71 | } 72 | return gm.getFull(id, gam, pm); 73 | } catch (err) { 74 | if (!err.t_code) err.t_code = 500; 75 | throw err; 76 | } 77 | }; 78 | 79 | export const adminEditGroup = async (db, group_id, active, req) => { 80 | const gm = new GroupManager(db); 81 | let group; 82 | try { 83 | if (isNaN(group_id)) { 84 | group = await gm.getByName(group_id); 85 | group_id = group.id; 86 | } else { 87 | group = await gm.get(group_id); 88 | } 89 | if (!group) { 90 | const error = new Error('Group not found'); 91 | error.t_code = 404; 92 | throw error; 93 | } 94 | if (!!group.active === active) { 95 | return false; 96 | } 97 | await gm.changeStatus(group_id, active); 98 | if (req && req.auditHelper) { 99 | req.auditHelper.log('groups', 'edit', group.name, { 100 | active: { prev: group.active ? 'true' : 'false', next: active ? 'true' : 'false' } 101 | }); 102 | } 103 | return true; 104 | } catch (err) { 105 | if (!err.t_code) err.t_code = 500; 106 | throw err; 107 | } 108 | }; 109 | 110 | export const adminDeleteGroup = async (db, group_id, req) => { 111 | const gm = new GroupManager(db); 112 | let group; 113 | try { 114 | if (isNaN(group_id)) { 115 | group = await gm.getByName(group_id); 116 | group_id = group.id; 117 | } else { 118 | group = await gm.get(group_id); 119 | } 120 | if (!group) { 121 | const error = new Error('Group not found'); 122 | error.t_code = 404; 123 | throw error; 124 | } 125 | await gm.delete(group_id); 126 | if (req && req.auditHelper) { 127 | req.auditHelper.log('groups', 'delete', group.name); 128 | } 129 | return true; 130 | } catch (err) { 131 | if (!err.t_code) err.t_code = 500; 132 | throw err; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /src/lib/helpers/admins/AdminGroupPermissionHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import GroupManager from '../../managers/GroupManager'; 16 | import PermissionManager from '../../managers/PermissionManager'; 17 | 18 | export const adminAddGroupPermission = async (db, group_id, user, host, ssh_options, req) => { 19 | if (!user) { 20 | const error = new Error('Malformed object, user is required'); 21 | error.code = 400; 22 | throw error; 23 | } 24 | if (!host) { 25 | const error = new Error('Malformed object, host is required'); 26 | error.code = 400; 27 | throw error; 28 | } 29 | const gm = new GroupManager(db); 30 | let group; 31 | try { 32 | if (isNaN(group_id)) { 33 | group = await gm.getByName(group_id); 34 | group_id = group.id; 35 | } else { 36 | group = await gm.get(group_id); 37 | } 38 | } catch (err) { 39 | err.t_code = 404; 40 | console.log('Throw 404'); 41 | throw err; 42 | } 43 | const pm = new PermissionManager(db); 44 | try { 45 | const permission_id = await pm.create(group_id, user, host, ssh_options); 46 | if (req && req.auditHelper) { 47 | req.auditHelper.log('groups', 'add_permission', group.name, { host, user, ssh_options }); 48 | } 49 | return { group_id, permission_id }; 50 | } catch (err) { 51 | err.t_code = 500; 52 | throw err; 53 | } 54 | }; 55 | 56 | export const adminDeleteGroupPermission = async (db, group_id, permission_id, req) => { 57 | const gm = new GroupManager(db); 58 | let group; 59 | try { 60 | if (isNaN(group_id)) { 61 | group = await gm.getByName(group_id); 62 | group_id = group.id; 63 | } else { 64 | group = await gm.get(group_id); 65 | } 66 | } catch (err) { 67 | err.t_code = 404; 68 | console.log('Throw 404'); 69 | throw err; 70 | } 71 | const pm = new PermissionManager(db); 72 | try { 73 | const permission = await pm.get(group_id, permission_id); 74 | if (!permission) { 75 | const error = new Error('Permission not found'); 76 | error.t_code = 404; 77 | throw error; 78 | } 79 | const ret = await pm.delete(group_id, permission_id); 80 | if (ret === 0) { 81 | const error = new Error('Permission not found'); 82 | error.t_code = 404; 83 | throw error; 84 | } 85 | const { host, user } = permission; 86 | if (req && req.auditHelper) { 87 | req.auditHelper.log('groups', 'remove_permission', group.name, { host, user }); 88 | } 89 | return true; 90 | } catch (err) { 91 | if (!err.t_code) err.t_code = 500; 92 | throw err; 93 | } 94 | }; 95 | 96 | export const adminUpdateGroupPermission = async (db, group_id, permission_id, ssh_options, req) => { 97 | const gm = new GroupManager(db); 98 | let group; 99 | try { 100 | if (isNaN(group_id)) { 101 | group = await gm.getByName(group_id); 102 | group_id = group.id; 103 | } else { 104 | group = await gm.get(group_id); 105 | } 106 | } catch (err) { 107 | err.t_code = 404; 108 | console.log('Throw 404'); 109 | throw err; 110 | } 111 | const pm = new PermissionManager(db); 112 | try { 113 | const permission = await pm.get(group_id, permission_id); 114 | if (!permission) { 115 | const error = new Error('Permission not found'); 116 | error.t_code = 404; 117 | throw error; 118 | } 119 | const ret = await pm.updateSSHOptions(group_id, permission_id, ssh_options); 120 | if (ret === 0) { 121 | const error = new Error('Permission not found'); 122 | error.t_code = 404; 123 | throw error; 124 | } 125 | const { host, user, ssh_options: ssh_options_old } = permission; 126 | if (req && req.auditHelper) { 127 | req.auditHelper.log('groups', 'update_permission', group.name, { 128 | host, 129 | user, 130 | ssh_options_old, 131 | ssh_options_new: ssh_options 132 | }); 133 | } 134 | return true; 135 | } catch (err) { 136 | if (!err.t_code) err.t_code = 500; 137 | throw err; 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /src/lib/helpers/admins/AdminPermissionHelper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import AccountManager from '../../managers/AccountManager'; 16 | import GroupManager from '../../managers/GroupManager'; 17 | import PermissionManager from '../../managers/PermissionManager'; 18 | import EventHelper from '../EventHelper'; 19 | 20 | export const adminAddAccountPermission = async (db, account_id, user, host, ssh_options, req) => { 21 | if (!user) { 22 | const error = new Error('Malformed object, user is required'); 23 | error.code = 400; 24 | throw error; 25 | } 26 | if (!host) { 27 | const error = new Error('Malformed object, host is required'); 28 | error.code = 400; 29 | throw error; 30 | } 31 | const am = new AccountManager(db); 32 | let account; 33 | try { 34 | if (isNaN(account_id)) { 35 | account_id = await am.getIdByEmail(account_id); 36 | } 37 | account = await am.get(account_id); 38 | account_id = account.id; 39 | } catch (err) { 40 | err.t_code = 404; 41 | console.log('Throw 404'); 42 | throw err; 43 | } 44 | const gm = new GroupManager(db); 45 | const group_id = await gm.getIdByName(account.email); 46 | if (!group_id) { 47 | console.error('Unable to get default group for %s', account.email); 48 | const error = new Error('Unable to get default group for ' + account.email); 49 | error.code = 500; 50 | throw error; 51 | } 52 | const pm = new PermissionManager(db); 53 | try { 54 | const permission_id = await pm.create(group_id, user, host, ssh_options); 55 | EventHelper.emit('theo:change', { 56 | func: 'account_permissions', 57 | action: 'add', 58 | object: account_id, 59 | receiver: 'admin' 60 | }); 61 | if (req && req.auditHelper) { 62 | req.auditHelper.log('accounts', 'add_permission', account.email, { 63 | user, 64 | host, 65 | ssh_options 66 | }); 67 | } 68 | return { account_id, permission_id }; 69 | } catch (err) { 70 | console.error(err); 71 | err.t_code = 500; 72 | throw err; 73 | } 74 | }; 75 | 76 | export const adminDeleteAccountPermission = async (db, account_id, permission_id, req) => { 77 | const am = new AccountManager(db); 78 | let account; 79 | try { 80 | if (isNaN(account_id)) { 81 | account = await am.getByEmail(account_id); 82 | } else { 83 | account = await am.get(account_id); 84 | } 85 | } catch (err) { 86 | err.t_code = 404; 87 | console.log('Throw 404'); 88 | throw err; 89 | } 90 | const gm = new GroupManager(db); 91 | const group_id = await gm.getIdByName(account.email); 92 | const pm = new PermissionManager(db); 93 | try { 94 | const permission = await pm.get(group_id, permission_id); 95 | if (!permission) { 96 | const error = new Error('Permission not found'); 97 | error.t_code = 404; 98 | throw error; 99 | } 100 | await pm.delete(group_id, permission_id); 101 | EventHelper.emit('theo:change', { 102 | func: 'account_permissions', 103 | action: 'delete', 104 | object: account.id, 105 | receiver: 'admin' 106 | }); 107 | if (req && req.auditHelper) { 108 | req.auditHelper.log('accounts', 'remove_permission', account.email, { 109 | user: permission.user, 110 | host: permission.host 111 | }); 112 | } 113 | return true; 114 | } catch (err) { 115 | if (!err.t_code) err.t_code = 500; 116 | throw err; 117 | } 118 | }; 119 | 120 | export const adminUpdateAccountPermission = async (db, account_id, permission_id, ssh_options, req) => { 121 | const am = new AccountManager(db); 122 | let account; 123 | try { 124 | if (isNaN(account_id)) { 125 | account = await am.getByEmail(account_id); 126 | } else { 127 | account = await am.get(account_id); 128 | } 129 | } catch (err) { 130 | err.t_code = 404; 131 | console.log('Throw 404'); 132 | throw err; 133 | } 134 | const gm = new GroupManager(db); 135 | const group_id = await gm.getIdByName(account.email); 136 | const pm = new PermissionManager(db); 137 | try { 138 | const permission = await pm.get(group_id, permission_id); 139 | if (!permission) { 140 | const error = new Error('Permission not found'); 141 | error.t_code = 404; 142 | throw error; 143 | } 144 | await pm.updateSSHOptions(group_id, permission_id, ssh_options); 145 | EventHelper.emit('theo:change', { 146 | func: 'account_permissions', 147 | action: 'update', 148 | object: account.id, 149 | receiver: 'admin' 150 | }); 151 | if (req && req.auditHelper) { 152 | req.auditHelper.log('accounts', 'update_permission', account.email, { 153 | user: permission.user, 154 | host: permission.host, 155 | ssh_options_old: permission.ssh_options, 156 | ssh_options_new: req.body.ssh_options 157 | }); 158 | } 159 | return true; 160 | } catch (err) { 161 | if (!err.t_code) err.t_code = 500; 162 | throw err; 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /src/lib/keys_importer/github/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import KeysImporterManager from '../../managers/KeysImporterManager'; 16 | import { http_get } from '../../utils/httpUtils'; 17 | 18 | class GithubImporter extends KeysImporterManager { 19 | async get(username) { 20 | try { 21 | const keys = await http_get(`https://api.github.com/users/${username}/keys`, { 22 | Accept: 'application/vnd.github.v3+json' 23 | }); 24 | return keys.map(key => { 25 | return key.key; 26 | }); 27 | } catch (err) { 28 | console.error('Failed to import from github', err); 29 | } 30 | } 31 | } 32 | 33 | export default GithubImporter; 34 | -------------------------------------------------------------------------------- /src/lib/keys_importer/gitlab/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import KeysImporterManager from '../../managers/KeysImporterManager'; 16 | import { http_get } from '../../utils/httpUtils'; 17 | 18 | class GitlabImporter extends KeysImporterManager { 19 | async get(username) { 20 | try { 21 | const res = await http_get(`https://gitlab.com/api/v4/users?username=${username}`); 22 | if (res && res.length === 1) { 23 | const keys = await http_get(`https://gitlab.com/api/v4/users/${res[0].id}/keys`); 24 | return keys.map(key => { 25 | return key.key; 26 | }); 27 | } else { 28 | return []; 29 | } 30 | } catch (err) { 31 | console.error('Failed to import from gitlab', err); 32 | } 33 | } 34 | } 35 | 36 | export default GitlabImporter; 37 | -------------------------------------------------------------------------------- /src/lib/keys_importer/modules.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Github from './github'; 16 | import Gitlab from './gitlab'; 17 | 18 | const modules = { 19 | github: Github, 20 | gitlab: Gitlab 21 | }; 22 | 23 | export const getKeysImporterModule = name => { 24 | return modules[name]; 25 | }; 26 | 27 | export const getKeysImporterModulesList = () => { 28 | return Object.keys(modules); 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/managers/AuthTokenManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { md5 } from '../utils/cryptoUtils'; 16 | 17 | class AuthTokenManager { 18 | constructor(db) { 19 | this.db = db; 20 | } 21 | 22 | async getAll() { 23 | const sql = 'select token, assignee, type from auth_tokens'; 24 | const tokens = await this.db.all(sql); 25 | const agentTokens = []; 26 | const adminTokens = {}; 27 | tokens.forEach(token => { 28 | if (token.type === 'agent') { 29 | agentTokens.push(token.token); 30 | } else if (token.type === 'admin') { 31 | adminTokens[token.token] = token.assignee; 32 | } 33 | }); 34 | return { 35 | admins: adminTokens, 36 | clients: agentTokens 37 | }; 38 | } 39 | 40 | create(token, type, assignee = '') { 41 | const sql = 'insert into auth_tokens (token, assignee, type, created_at) values (?, ?, ?, ?) '; 42 | return this.db.run(sql, [token, assignee, type, new Date().getTime()]); 43 | } 44 | 45 | delete() { 46 | const sql = 'delete from auth_tokens'; 47 | return this.db.delete(sql); 48 | } 49 | 50 | static getTokenAssignee(admin, as_admin = false) { 51 | let token; 52 | let assignee; 53 | if (typeof admin === 'string') { 54 | console.error('[WARN] tokens.admin as string is deprecated. Please see documentation'); 55 | if (as_admin) { 56 | console.error('[WARN] We will use "admin" as assignee'); 57 | assignee = 'admin'; 58 | } else { 59 | console.error('[WARN] We will use md5(tokens.admin) as assignee'); 60 | assignee = md5(admin); 61 | } 62 | token = admin; 63 | } else { 64 | token = admin.token; 65 | assignee = admin.assignee; 66 | } 67 | return { token, assignee }; 68 | } 69 | } 70 | 71 | export default AuthTokenManager; 72 | -------------------------------------------------------------------------------- /src/lib/managers/BaseCacheManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { loadCacheManager } from '../helpers/CacheHelper'; 16 | 17 | let _cm; 18 | 19 | export const MAX_ROWS = 100; 20 | 21 | class BaseCacheManager { 22 | constructor(db) { 23 | this.db = db; 24 | } 25 | 26 | invalidateCache() { 27 | if (_cm === undefined) { 28 | _cm = loadCacheManager(); 29 | } 30 | if (_cm !== false) { 31 | _cm.flush().catch(err => { 32 | console.error('Failed to flush cache', err.message); 33 | }); 34 | } 35 | } 36 | } 37 | 38 | export default BaseCacheManager; 39 | -------------------------------------------------------------------------------- /src/lib/managers/CacheManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class CachedManager { 16 | constructor(options) { 17 | this.options = options; 18 | } 19 | 20 | get(key) { 21 | throw new Error('Not implemented'); 22 | } 23 | 24 | set(key, value) { 25 | throw new Error('Not implemented'); 26 | } 27 | 28 | del(key) { 29 | throw new Error('Not implemented'); 30 | } 31 | 32 | flush() { 33 | throw new Error('Not implemented'); 34 | } 35 | 36 | quit() { 37 | throw new Error('Not implemented'); 38 | } 39 | } 40 | 41 | export default CachedManager; 42 | -------------------------------------------------------------------------------- /src/lib/managers/DbManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class DbManager { 16 | constructor(options) { 17 | this.options = options; 18 | } 19 | 20 | getEngine() { 21 | throw new Error('Not implemented'); 22 | } 23 | 24 | initDb() { 25 | throw new Error('Not implemented'); 26 | } 27 | 28 | upgradeDb(fromVersion) { 29 | throw new Error('Not implemented'); 30 | } 31 | 32 | getCurrentVersion() { 33 | throw new Error('Not implemented'); 34 | } 35 | 36 | createVersionTable() { 37 | throw new Error('Not implemented'); 38 | } 39 | 40 | flushDb() { 41 | throw new Error('Not implemented'); 42 | } 43 | 44 | getClient(pool = false) { 45 | throw new Error('Not implemented'); 46 | } 47 | 48 | setClient(client) { 49 | throw new Error('Not implemented'); 50 | } 51 | 52 | close() { 53 | throw new Error('Not implemented'); 54 | } 55 | } 56 | 57 | export default DbManager; 58 | -------------------------------------------------------------------------------- /src/lib/managers/GroupAccountManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import GroupManager from './GroupManager'; 16 | 17 | class GroupAccountManager { 18 | constructor(db, gm) { 19 | this.db = db; 20 | if (gm) { 21 | this.gm = gm; 22 | } else { 23 | this.gm = new GroupManager(this.db); 24 | } 25 | } 26 | 27 | getAll(group_id, limit, offset) { 28 | let sql = 29 | 'select ga.id, g.id, g.name, g.active from groups_accounts ga, tgroups g where g.id = ga.group_id and g.id = ? order by name asc'; 30 | if (limit) { 31 | sql += ' limit ' + limit; 32 | } 33 | if (offset) { 34 | sql += ' offset ' + offset; 35 | } 36 | return this.db.all(sql, [group_id]); 37 | } 38 | 39 | getAllAccounts(group_id, limit, offset) { 40 | let sql = 41 | 'select a.id, a.name, a.email, a.active, a.expire_at, a.created_at from groups_accounts ga, accounts a where a.id = ga.account_id and ga.group_id = ? order by a.name asc'; 42 | if (limit) { 43 | sql += ' limit ' + limit; 44 | } 45 | if (offset) { 46 | sql += ' offset ' + offset; 47 | } 48 | return this.db.all(sql, [group_id]); 49 | } 50 | 51 | getAllByAccount(account_id, limit, offset) { 52 | let sql = 53 | 'select ga.id, g.id, g.name, g.active, g.is_internal, g.created_at from groups_accounts ga, tgroups g where g.id = ga.group_id and ga.account_id = ? order by name asc'; 54 | if (limit) { 55 | sql += ' limit ' + limit; 56 | } 57 | if (offset) { 58 | sql += ' offset ' + offset; 59 | } 60 | return this.db.all(sql, [account_id]); 61 | } 62 | 63 | async create(group_id, account_id, skip_invalidation = false) { 64 | const sql = 'insert into groups_accounts (group_id, account_id, created_at) values (?, ?, ?) '; 65 | const lastId = await this.db.insert(sql, [group_id, account_id, new Date().getTime()]); 66 | if (!skip_invalidation) { 67 | await this.gm.setUpdatedAt(group_id); 68 | } 69 | return lastId; 70 | } 71 | 72 | async delete(group_id, account_id, skip_invalidation = false) { 73 | const sql = 'delete from groups_accounts where group_id = ? and account_id = ?'; 74 | const changes = await this.db.delete(sql, [group_id, account_id]); 75 | if (changes > 0) { 76 | if (!skip_invalidation) { 77 | await this.gm.setUpdatedAt(group_id); 78 | } 79 | } 80 | return changes; 81 | } 82 | } 83 | 84 | export default GroupAccountManager; 85 | -------------------------------------------------------------------------------- /src/lib/managers/KeyManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { parseKeySSHOptions } from '../utils/sshOptionsUtils'; 16 | 17 | class KeyManager { 18 | constructor(db, am) { 19 | this.db = db; 20 | if (am) { 21 | this.am = am; 22 | } else { 23 | throw new Error('Missing required AccountManager parameter'); 24 | } 25 | } 26 | 27 | async getAll(account_id, limit, offset) { 28 | let sql = 29 | 'select id, public_key, fingerprint, public_key_sig, key_ssh_options, last_used_at, created_at from public_keys where account_id = ? order by created_at '; 30 | if (limit) { 31 | sql += ' limit ' + limit; 32 | } 33 | if (offset) { 34 | sql += ' offset ' + offset; 35 | } 36 | const rows = await this.db.all(sql, [account_id]); 37 | return rows.map(parseKeySSHOptions); 38 | } 39 | 40 | async create(account_id, key, fingerprint, signature = null, ssh_options = '') { 41 | const sql = 42 | 'insert into public_keys (account_id, public_key, fingerprint, public_key_sig, key_ssh_options, created_at) values (?, ?, ?, ?, ?, ?) '; 43 | const id = await this.db.insert(sql, [account_id, key, fingerprint, signature, ssh_options, new Date().getTime()]); 44 | await this.am.setUpdatedAt(account_id); 45 | return id; 46 | } 47 | 48 | async delete(account_id, id) { 49 | const sql = 'delete from public_keys where id = ? and account_id = ?'; 50 | const changes = await this.db.delete(sql, [id, account_id]); 51 | await this.am.setUpdatedAt(account_id); 52 | return changes; 53 | } 54 | 55 | async update(account_id, id, ssh_options = '') { 56 | const sql = 'update public_keys set key_ssh_options = ? where id = ? and account_id = ?'; 57 | const changes = await this.db.update(sql, [ssh_options, id, account_id]); 58 | await this.am.setUpdatedAt(account_id); 59 | return changes; 60 | } 61 | 62 | async get(account_id, id) { 63 | const sql = 64 | 'select id, public_key, fingerprint, public_key_sig, key_ssh_options from public_keys where id = ? and account_id = ?'; 65 | const row = await this.db.get(sql, [id, account_id]); 66 | return parseKeySSHOptions(row); 67 | } 68 | 69 | async checkFingerprint(fingerprint) { 70 | const sql = 'select id, account_id from public_keys where fingerprint = ?'; 71 | return this.db.get(sql, [fingerprint]); 72 | } 73 | 74 | static setLastUsed(dbConn, fingerprint) { 75 | const sql = 'update public_keys set last_used_at = ? where fingerprint = ? '; 76 | return dbConn.update(sql, [Date.now(), fingerprint]); 77 | } 78 | } 79 | 80 | export default KeyManager; 81 | -------------------------------------------------------------------------------- /src/lib/managers/KeysImporterManager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class KeysImporterManager { 16 | constructor(options) { 17 | this.options = options; 18 | } 19 | 20 | get(username) { 21 | throw new Error('Not implemented'); 22 | } 23 | } 24 | 25 | export default KeysImporterManager; 26 | -------------------------------------------------------------------------------- /src/lib/middlewares/AuditMiddleware.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import AuditHelper from '../helpers/AuditHelper'; 16 | 17 | export const auditMiddleware = function (req, res, next) { 18 | req.auditHelper = new AuditHelper(req); 19 | next(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/middlewares/AuthMiddleware.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import AppHelper from '../helpers/AppHelper'; 16 | import EventHelper from '../helpers/EventHelper'; 17 | 18 | const checkForBearer = function (req, header) { 19 | const authorization = req.header('Authorization'); 20 | if (authorization) { 21 | const m = /^[Bb]earer\s+(\S+)$/.exec(authorization); 22 | if (m === null) { 23 | return; 24 | } 25 | const [, token] = m; 26 | return token; 27 | } 28 | }; 29 | 30 | export const authMiddleware = (req, res, next) => { 31 | const token = checkForBearer(req, 'Authorization'); 32 | if (token) { 33 | try { 34 | let gotcb = false; 35 | const done = EventHelper.emit('theo:authorize', token, (err, auth) => { 36 | if (gotcb) return; 37 | gotcb = true; 38 | if (!err && auth) { 39 | req.is_authorized = true; 40 | req.is_admin = auth.is_admin || false; 41 | req.is_core = auth.is_core || false; 42 | if (!req.auth_token) { 43 | if (req.is_core) { 44 | const onBehalfOfToken = req.header('X-On-Behalf-Of'); 45 | if (onBehalfOfToken) { 46 | req.auth_token = onBehalfOfToken; 47 | } else { 48 | req.auth_token = 'core'; 49 | } 50 | } else if (req.is_admin) { 51 | req.auth_token = 'admin'; 52 | } 53 | } 54 | } 55 | next(); 56 | }); 57 | if (!done) { 58 | const _sm = AppHelper(); 59 | const _settings = _sm.getSettings(); 60 | if (_settings.core && token === _settings.core.token) { 61 | req.is_authorized = true; 62 | req.is_admin = true; 63 | req.is_core = true; 64 | const onBehalfOfToken = req.header('X-On-Behalf-Of'); 65 | if (onBehalfOfToken) { 66 | req.auth_token = onBehalfOfToken; 67 | } else { 68 | req.auth_token = 'core'; 69 | } 70 | } else if (token === _settings.admin.token) { 71 | req.is_authorized = true; 72 | req.is_admin = true; 73 | req.auth_token = 'admin'; 74 | } else if (_settings.admin.tokens && _settings.admin.tokens[token]) { 75 | req.is_authorized = true; 76 | req.is_admin = true; 77 | req.auth_token = _settings.admin.tokens[token]; 78 | } else { 79 | if (_settings.client.tokens) { 80 | if (_settings.client.tokens.includes(token)) { 81 | req.is_authorized = true; 82 | req.auth_token = token; 83 | } 84 | } 85 | } 86 | next(); 87 | } 88 | } catch (e) { 89 | next(); 90 | } 91 | return; 92 | } 93 | next(); 94 | }; 95 | 96 | export const requireCoreAuthMiddleware = (req, res, next) => { 97 | if (!req.is_authorized || !req.is_core) { 98 | res.status(401); 99 | res.json({ status: 401, reason: 'Unauthorized' }); 100 | return; 101 | } 102 | next(); 103 | }; 104 | 105 | export const requireAdminAuthMiddleware = (req, res, next) => { 106 | if (!req.is_authorized || !req.is_admin) { 107 | res.status(401); 108 | res.json({ status: 401, reason: 'Unauthorized' }); 109 | return; 110 | } 111 | next(); 112 | }; 113 | 114 | export const requireAuthMiddleware = (req, res, next) => { 115 | if (!req.is_authorized) { 116 | res.status(401); 117 | res.json({ status: 401, reason: 'Unauthorized' }); 118 | return; 119 | } 120 | next(); 121 | }; 122 | -------------------------------------------------------------------------------- /src/lib/rdbms/baseclient.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class BaseClient { 16 | getServerVersion() { 17 | throw new Error('Not implemented'); 18 | } 19 | 20 | all(sql, params) { 21 | throw new Error('Not implemented'); 22 | } 23 | 24 | get(sql, params) { 25 | throw new Error('Not implemented'); 26 | } 27 | 28 | run(sql, params) { 29 | throw new Error('Not implemented'); 30 | } 31 | 32 | insert(sql, params) { 33 | throw new Error('Not implemented'); 34 | } 35 | 36 | update(sql, params) { 37 | throw new Error('Not implemented'); 38 | } 39 | 40 | delete(sql, params) { 41 | throw new Error('Not implemented'); 42 | } 43 | 44 | open() { 45 | throw new Error('Not implemented'); 46 | } 47 | 48 | close() { 49 | throw new Error('Not implemented'); 50 | } 51 | } 52 | 53 | export default BaseClient; 54 | -------------------------------------------------------------------------------- /src/lib/rdbms/modules.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { common_info } from '../utils/logUtils'; 16 | 17 | const modules = {}; 18 | 19 | try { 20 | const Mariadb = require('./mariadb'); 21 | modules.mariadb = Mariadb.default; 22 | modules.mysql = Mariadb.default; 23 | } catch (e) { 24 | common_info('mariadb driver not loaded'); 25 | } 26 | try { 27 | const Sqlite = require('./sqlite'); 28 | modules.sqlite = Sqlite.default; 29 | } catch (e) { 30 | common_info('sqlite driver not loaded'); 31 | } 32 | 33 | export const getRdbmsModule = name => { 34 | return modules[name]; 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/rdbms/sqlite/client.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import BaseClient from '../baseclient'; 16 | 17 | class SqliteClient extends BaseClient { 18 | constructor(db) { 19 | super(); 20 | this.db = db; 21 | } 22 | 23 | getServerVersion() { 24 | return 'DB sqlite'; 25 | } 26 | 27 | all(sql, params) { 28 | return new Promise((resolve, reject) => { 29 | this.db.all(sql, params, (err, rows) => { 30 | if (err) { 31 | return reject(err); 32 | } 33 | return resolve(rows); 34 | }); 35 | }); 36 | } 37 | 38 | get(sql, params) { 39 | return new Promise((resolve, reject) => { 40 | this.db.get(sql, params, (err, row) => { 41 | if (err) { 42 | return reject(err); 43 | } 44 | return resolve(row); 45 | }); 46 | }); 47 | } 48 | 49 | run(sql, params) { 50 | return new Promise((resolve, reject) => { 51 | this.db.get(sql, params, err => { 52 | if (err) { 53 | return reject(err); 54 | } 55 | return resolve(true); 56 | }); 57 | }); 58 | } 59 | 60 | insert(sql, params) { 61 | return new Promise((resolve, reject) => { 62 | this.db.run(sql, params, function (err) { 63 | if (err) { 64 | reject(err); 65 | return; 66 | } 67 | resolve(this.lastID); 68 | }); 69 | }); 70 | } 71 | 72 | update(sql, params) { 73 | return new Promise((resolve, reject) => { 74 | this.db.run(sql, params, function (err) { 75 | if (err) { 76 | reject(err); 77 | return; 78 | } 79 | resolve(this.changes); 80 | }); 81 | }); 82 | } 83 | 84 | delete(sql, params) { 85 | return new Promise((resolve, reject) => { 86 | this.db.run(sql, params, function (err) { 87 | if (err) { 88 | return reject(err); 89 | } 90 | resolve(this.changes); 91 | }); 92 | }); 93 | } 94 | 95 | open() { 96 | // Do nothing 97 | return Promise.resolve(); 98 | } 99 | 100 | close() { 101 | // Do nothing 102 | return Promise.resolve(); 103 | } 104 | } 105 | 106 | export default SqliteClient; 107 | -------------------------------------------------------------------------------- /src/lib/utils/cryptoUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import crypto from 'crypto'; 16 | 17 | export const md5 = function (data) { 18 | return hash('md5', data); 19 | }; 20 | 21 | export const sha256 = function (data) { 22 | return hash('sha256', data); 23 | }; 24 | 25 | export const hash = function (type, data) { 26 | return crypto.createHash(type).update(data).digest('hex'); 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/utils/dateUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const getTimestampFromISO8601 = string => { 16 | if (!string) { 17 | return 0; 18 | } 19 | let ret = 0; 20 | const type = typeof string; 21 | if (type === 'string') { 22 | const d = new Date(string); 23 | const ts = d.getTime(); 24 | if (isNaN(ts)) { 25 | throw new Error('Invalid date string'); 26 | } 27 | ret = ts; 28 | } else if (type === 'object') { 29 | if (string instanceof Date) { 30 | ret = string.getTime(); 31 | } else { 32 | throw new Error('Invalid object'); 33 | } 34 | } else { 35 | ret = string; 36 | } 37 | return ret; 38 | }; 39 | 40 | export function millisecondsToStr(milliseconds) { 41 | function numberEnding(number) { 42 | return number > 1 ? 's' : ''; 43 | } 44 | let temp = Math.floor(milliseconds / 1000); 45 | const years = Math.floor(temp / 31536000); 46 | if (years) { 47 | return years + ' year' + numberEnding(years); 48 | } 49 | const days = Math.floor((temp %= 31536000) / 86400); 50 | if (days) { 51 | return days + ' day' + numberEnding(days); 52 | } 53 | const hours = Math.floor((temp %= 86400) / 3600); 54 | if (hours) { 55 | return hours + ' hour' + numberEnding(hours); 56 | } 57 | const minutes = Math.floor((temp %= 3600) / 60); 58 | if (minutes) { 59 | return minutes + ' minute' + numberEnding(minutes); 60 | } 61 | const seconds = temp % 60; 62 | if (seconds) { 63 | return seconds + ' second' + numberEnding(seconds); 64 | } 65 | return 'less than a second'; 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/utils/dnsUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import dns from 'dns'; 16 | 17 | export const dnsReverse = function (ip) { 18 | return new Promise((resolve, reject) => { 19 | dns.reverse(ip, function (err, res) { 20 | if (err) { 21 | reject(err); 22 | return; 23 | } 24 | resolve(res); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/utils/fsUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import fs from 'fs'; 16 | import path from 'path'; 17 | 18 | export const getDirFiles = (dir, onlyDir) => { 19 | try { 20 | const stat = fs.statSync(dir); 21 | if (!stat.isDirectory()) { 22 | return false; 23 | } 24 | } catch (e) { 25 | return false; 26 | } 27 | let files = fs.readdirSync(dir); 28 | if (onlyDir) { 29 | files = files.filter(file => { 30 | const stat = fs.statSync(path.join(dir, file)); 31 | return stat.isDirectory(); 32 | }); 33 | } 34 | return files; 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/utils/httpUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const fetch = require('node-fetch'); 16 | 17 | const execute = (method, path, data, headers = {}) => { 18 | const fetchOpts = { 19 | method, 20 | headers 21 | }; 22 | const dataType = typeof data; 23 | if (dataType !== 'undefined') { 24 | if (dataType !== 'string') { 25 | data = JSON.stringify(data); 26 | if (!fetchOpts.headers['Content-Type']) { 27 | fetchOpts.headers['Content-Type'] = 'application/json'; 28 | } 29 | } 30 | fetchOpts.body = data; 31 | } 32 | return fetch(path, fetchOpts).then(res => { 33 | if (res.status >= 400) { 34 | const error = new Error(res.statusText); 35 | error.code = res.status; 36 | error.http_response = res; 37 | throw error; 38 | } 39 | const contentType = res.headers.get('content-type'); 40 | if (contentType.indexOf('application/json') >= 0) { 41 | return res.json(); 42 | } 43 | return res.text(); 44 | }); 45 | }; 46 | 47 | export const http_get = (path, headers) => { 48 | return execute('GET', path, undefined, headers); 49 | }; 50 | 51 | export const http_post = (path, data, headers) => { 52 | return execute('POST', path, data, headers); 53 | }; 54 | -------------------------------------------------------------------------------- /src/lib/utils/logUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import util from 'util'; 16 | 17 | export const DEBUG = 'DEBUG'; 18 | export const INFO = 'INFO'; 19 | export const WARN = 'WARN'; 20 | export const ERROR = 'ERROR'; 21 | 22 | const LEVELS = { 23 | DEBUG: 8, 24 | INFO: 4, 25 | WARN: 2, 26 | ERROR: 1 27 | }; 28 | 29 | let LOG_LEVEL; 30 | 31 | let logger; 32 | 33 | const defaultLogger = function (type, date, msg) { 34 | console.log('[ %s ][ %s ] %s', type, date, msg); 35 | }; 36 | 37 | export const initLogger = function (level = false, logfn = undefined) { 38 | if (typeof level === 'function') { 39 | logfn = level; 40 | level = false; 41 | } 42 | if (!logfn) { 43 | logger = defaultLogger; 44 | } else { 45 | logger = logfn; 46 | } 47 | const LOG_LEVEL_D = level || process.env.LOG_LEVEL || 'INFO'; 48 | switch (LOG_LEVEL_D.toUpperCase()) { 49 | case DEBUG: 50 | LOG_LEVEL = LEVELS.DEBUG + LEVELS.INFO + LEVELS.WARN + LEVELS.ERROR; 51 | break; 52 | case INFO: 53 | LOG_LEVEL = LEVELS.INFO + LEVELS.WARN + LEVELS.ERROR; 54 | break; 55 | case WARN: 56 | LOG_LEVEL = LEVELS.WARN + LEVELS.ERROR; 57 | break; 58 | case ERROR: 59 | LOG_LEVEL = LEVELS.ERROR; 60 | break; 61 | } 62 | }; 63 | 64 | export const common_log = function (type, message, args) { 65 | if (LEVELS[type] > LOG_LEVEL) { 66 | return; 67 | } 68 | const msg = util.format.apply(null, [message, ...args]); 69 | if (!logger) { 70 | logger = defaultLogger; 71 | } 72 | logger(type + (type.length < ERROR.length ? ' ' : ''), new Date().toISOString(), msg); 73 | }; 74 | 75 | export const common_debug = function (message, ...args) { 76 | common_log(DEBUG, message, args); 77 | }; 78 | 79 | export const common_info = function (message, ...args) { 80 | common_log(INFO, message, args); 81 | }; 82 | 83 | export const common_warn = function (message, ...args) { 84 | common_log(WARN, message, args); 85 | }; 86 | 87 | export const common_error = function (message, ...args) { 88 | common_log(ERROR, message, args); 89 | }; 90 | -------------------------------------------------------------------------------- /src/lib/utils/processUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const setTimeoutPromise = function (timeout) { 16 | return new Promise((resolve, reject) => { 17 | setTimeout(function () { 18 | resolve(); 19 | }, timeout); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/utils/sshOptionsUtils.js: -------------------------------------------------------------------------------- 1 | export const parseSSHOptions = function (row) { 2 | if (row.ssh_options) { 3 | row.ssh_options = JSON.parse(row.ssh_options); 4 | } else { 5 | row.ssh_options = false; 6 | } 7 | return row; 8 | }; 9 | 10 | export const parseKeySSHOptions = function (row) { 11 | if (row.key_ssh_options) { 12 | row.key_ssh_options = JSON.parse(row.key_ssh_options); 13 | } else { 14 | row.key_ssh_options = false; 15 | } 16 | return row; 17 | }; 18 | 19 | export const calculateDistance = function (matchLen, r) { 20 | const permissionLen = r.host.length + r.user.length; 21 | const distance = matchLen - permissionLen; 22 | r.distance = distance <= 0 ? 0 : distance; 23 | }; 24 | 25 | export const mergeSSHOptions = function (a, b) { 26 | if (!a) { 27 | return b; 28 | } 29 | if (!b) { 30 | return a; 31 | } 32 | const ret = {}; 33 | if (a.from || b.from) { 34 | ret.from = [...new Set((a.from || []).concat(b.from || []))]; 35 | } 36 | if (a.permitopen || b.permitopen) { 37 | ret.permitopen = [...new Set((a.permitopen || []).concat(b.permitopen || []))]; 38 | } 39 | if (a.environment || b.environment) { 40 | ret.environment = [...new Set((a.environment || []).concat(b.environment || []))]; 41 | } 42 | if (a.command || b.command) { 43 | ret.command = a.command || b.command; 44 | } 45 | if (a.restrict || b.restrict) { 46 | ret.restrict = true; 47 | } 48 | if (a['no-agent-forwarding'] || b['no-agent-forwarding']) { 49 | ret['no-agent-forwarding'] = true; 50 | } 51 | if (a['agent-forwarding'] || b['agent-forwarding']) { 52 | ret['agent-forwarding'] = true; 53 | } 54 | if (a['no-port-forwarding'] || b['no-port-forwarding']) { 55 | ret['no-port-forwarding'] = true; 56 | } 57 | if (a['port-forwarding'] || b['port-forwarding']) { 58 | ret['port-forwarding'] = true; 59 | } 60 | if (a['no-pty'] || b['no-pty']) { 61 | ret['no-pty'] = true; 62 | } 63 | if (a.pty || b.pty) { 64 | ret.pty = true; 65 | } 66 | if (a['no-user-rc'] || b['no-user-rc']) { 67 | ret['no-user-rc'] = true; 68 | } 69 | if (a['user-rc'] || b['user-rc']) { 70 | ret['user-rc'] = true; 71 | } 72 | if (a['no-X11-forwarding'] || b['no-X11-forwarding']) { 73 | ret['no-X11-forwarding'] = true; 74 | } 75 | if (a['X11-forwarding'] || b['X11-forwarding']) { 76 | ret['X11-forwarding'] = true; 77 | } 78 | return ret; 79 | }; 80 | 81 | export const renderSSHOptions = function (ssh_options) { 82 | const ret = []; 83 | if (ssh_options.from?.length > 0) { 84 | ret.push(`from="${ssh_options.from.join(',')}"`); 85 | } 86 | if (ssh_options.permitopen?.length > 0) { 87 | ret.push(`permitopen="${ssh_options.permitopen.join(',')}"`); 88 | } 89 | if (ssh_options.environment?.length > 0) { 90 | ret.push(`environment="${ssh_options.environment.join(',')}"`); 91 | } 92 | if (ssh_options.command) { 93 | ret.push(`command="${ssh_options.command}"`); 94 | } 95 | if (ssh_options.restrict) { 96 | ret.push('restrict'); 97 | if (ssh_options['agent-forwarding']) { 98 | ret.push('agent-forwarding'); 99 | } 100 | if (ssh_options['port-forwarding']) { 101 | ret.push('port-forwarding'); 102 | } 103 | if (ssh_options.pty) { 104 | ret.push('pty'); 105 | } 106 | if (ssh_options['user-rc']) { 107 | ret.push('user-rc'); 108 | } 109 | if (ssh_options['X11-forwarding']) { 110 | ret.push('X11-forwarding'); 111 | } 112 | } else { 113 | if (ssh_options['no-agent-forwarding']) { 114 | ret.push('no-agent-forwarding'); 115 | } 116 | if (ssh_options['no-port-forwarding']) { 117 | ret.push('no-port-forwarding'); 118 | } 119 | if (ssh_options['no-pty']) { 120 | ret.push('no-pty'); 121 | } 122 | if (ssh_options['no-user-rc']) { 123 | ret.push('no-user-rc'); 124 | } 125 | if (ssh_options['no-X11-forwarding']) { 126 | ret.push('no-X11-forwarding'); 127 | } 128 | } 129 | return ret.join(','); 130 | }; 131 | -------------------------------------------------------------------------------- /src/lib/utils/sshUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import sshpk from 'sshpk'; 16 | import { common_debug } from './logUtils'; 17 | 18 | /** 19 | * @return {string} 20 | */ 21 | export const SSHFingerprint = function (keyPub) { 22 | const key = sshpk.parseKey(keyPub, 'ssh'); 23 | return key.fingerprint().toString(); 24 | }; 25 | 26 | export const getSSH2Comment = function (keyPub) { 27 | let comment = ''; 28 | keyPub.split('\n').forEach(line => { 29 | if (line.toLowerCase().indexOf('comment:') === 0) { 30 | const _comment = line.substring(8).trim(); 31 | if (_comment[0] === '"') { 32 | comment = _comment.substring(1, _comment.length - 1); 33 | } else { 34 | comment = _comment; 35 | } 36 | } 37 | }); 38 | return comment; 39 | }; 40 | 41 | export const getOpenSSHPublicKey = function (keyPub, signRequired) { 42 | keyPub = keyPub.trim(); 43 | const key = sshpk.parseKey(keyPub); 44 | let openssh = key.toString('ssh'); 45 | if (!openssh) { 46 | common_debug('Key %s is not a valid OpenSSH Public Key', keyPub); 47 | return false; 48 | } 49 | if (openssh !== keyPub) { 50 | if (signRequired) { 51 | throw new Error('You must sign an OpenSSH Public Key'); 52 | } 53 | if (key.comment === '(unnamed)') { 54 | if (keyPub.indexOf('---- BEGIN SSH2 PUBLIC KEY ----') === 0) { 55 | key.comment = getSSH2Comment(keyPub); 56 | openssh = key.toString('ssh'); 57 | } 58 | } 59 | } 60 | return openssh; 61 | }; 62 | -------------------------------------------------------------------------------- /src/migrations/v10fixGroups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const runV10migrationMariaDb = async client => { 16 | await client.run( 17 | 'insert into tgroups (id, name, active, updated_at, created_at) select id, name, active, updated_at, created_at from groups' 18 | ); 19 | const dropFKSql = 'alter table groups_accounts DROP FOREIGN KEY groups_accounts_ibfk_1'; 20 | await client.run(dropFKSql); 21 | const createFKSql = 22 | 'alter table groups_accounts add CONSTRAINT groups_accounts_group_id FOREIGN KEY(group_id) REFERENCES tgroups (id) ON DELETE CASCADE'; 23 | await client.run(createFKSql); 24 | 25 | const dropFKSql2 = 'alter table permissions DROP FOREIGN KEY permissions_group_id'; 26 | await client.run(dropFKSql2); 27 | const createFKSql2 = 28 | 'alter table permissions add CONSTRAINT permissions_group_id FOREIGN KEY(group_id) REFERENCES tgroups (id) ON DELETE CASCADE'; 29 | await client.run(createFKSql2); 30 | await client.run('drop table groups'); 31 | }; 32 | -------------------------------------------------------------------------------- /src/migrations/v12fixFingerprints.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { SSHFingerprint } from '../lib/utils/sshUtils'; 16 | 17 | export const runV12migration = async client => { 18 | const rows = await client.all('select id, public_key from public_keys'); 19 | for (let i = 0; i < rows.length; i++) { 20 | const { public_key, id } = rows[i]; 21 | const fp = SSHFingerprint(public_key); 22 | await client.run('update public_keys set fingerprint = ? where id = ? ', [fp, id]); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/migrations/v7fixGroups.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { adminCreateGroup, adminCreateGroupAccount } from '../lib/helpers/AdminHelper'; 16 | 17 | export const runV7migrationMariaDb = async client => { 18 | const sql = 'select distinct account_id from permissions where group_id is null'; 19 | const accountsToFix = await client.all(sql); 20 | for (let i = 0; i < accountsToFix.length; i++) { 21 | const sqlAccount = 'select email from accounts where id = ?'; 22 | const account_id = accountsToFix[i].account_id; 23 | const account = await client.get(sqlAccount, [account_id]); 24 | const group_id = await adminCreateGroup(client, { name: account.email }, 'internal_db_upgrade', true); 25 | await adminCreateGroupAccount(client, group_id, account_id); 26 | const updateSql = 'update permissions set group_id = ? where account_id = ? '; 27 | await client.run(updateSql, [group_id, account_id]); 28 | } 29 | try { 30 | const dropFKSql = 'alter table permissions DROP FOREIGN KEY permissions_account_id'; 31 | await client.run(dropFKSql); 32 | } catch (e) { 33 | const dropFKSql = 'alter table permissions DROP FOREIGN KEY permissions_ibfk_2'; 34 | await client.run(dropFKSql); 35 | } 36 | const dropSql = 'alter table permissions drop column account_id'; 37 | await client.run(dropSql); 38 | }; 39 | 40 | export const runV7migrationSqliteDb = async client => { 41 | const sql = 'select distinct account_id from permissions where group_id is null'; 42 | const accountsToFix = await client.all(sql); 43 | for (let i = 0; i < accountsToFix.length; i++) { 44 | const account_id = accountsToFix[i].account_id; 45 | const sqlAccount = 'select email from accounts where id = ?'; 46 | const account = await client.get(sqlAccount, [account_id]); 47 | console.log('Creating group ', account.email); 48 | const group_id = await adminCreateGroup(client, { name: account.email }, 'internal_db_upgrade', true); 49 | console.log('Associating group %s with account %s', group_id, account_id); 50 | await adminCreateGroupAccount(client, group_id, account_id); 51 | console.log('Setting permissions to group %s from account %s', group_id, account_id); 52 | const updateSql = 'update permissions set group_id = ? where account_id = ? '; 53 | await client.run(updateSql, [group_id, account_id]); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/routes/impexp.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { requireAdminAuthMiddleware } from '../lib/middlewares/AuthMiddleware'; 16 | import { exp, imp } from '../lib/helpers/ImpExpHelper'; 17 | 18 | export default function handleImpExp(express) { 19 | const router = express.Router(); 20 | router.get('/', requireAdminAuthMiddleware, async (req, res, next) => { 21 | try { 22 | const dump = await exp(req.db); 23 | res.json(dump); 24 | } catch (err) { 25 | if (process.env.MODE === 'test') { 26 | console.error('Failed to export', err); 27 | } 28 | res.status(err.t_code || 500); 29 | res.json({ status: err.t_code || 500, reason: err.message }); 30 | } 31 | }); 32 | 33 | router.post('/', requireAdminAuthMiddleware, async (req, res, next) => { 34 | try { 35 | await imp(req.db, req.body); 36 | res.json(204); 37 | } catch (err) { 38 | console.log('Import failed ', err); 39 | res.status(err.t_code || 500); 40 | res.json({ status: err.t_code || 500, reason: err.message }); 41 | } 42 | }); 43 | return router; 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import os from 'os'; 16 | import packageJson from '../../package'; 17 | import handleAccounts from './accounts'; 18 | import handleKeys from './keys'; 19 | import handleGroups from './groups'; 20 | import { requireAdminAuthMiddleware, requireCoreAuthMiddleware } from '../lib/middlewares/AuthMiddleware'; 21 | import DbHelper from '../lib/helpers/DbHelper'; 22 | import AppHelper from '../lib/helpers/AppHelper'; 23 | import handlePermissions from './permissions'; 24 | import handleImpExp from './impexp'; 25 | import AuthTokenManager from '../lib/managers/AuthTokenManager'; 26 | import { loadCacheManager } from '../lib/helpers/CacheHelper'; 27 | import { millisecondsToStr } from '../lib/utils/dateUtils'; 28 | 29 | const notifyReloadTokens = function () { 30 | setImmediate(async () => { 31 | const cm = loadCacheManager(); 32 | try { 33 | const client = await cm.open(); 34 | client.publish('core_tokens', 'flush_tokens'); 35 | cm.close(client); 36 | } catch (e) { 37 | console.error('Error while flushing cache tokens', e); 38 | } 39 | }); 40 | }; 41 | 42 | const uptime_start = Date.now(); 43 | 44 | export const initRoutes = express => { 45 | const router = express.Router(); 46 | router.get('/', (req, res) => { 47 | const { name, description } = packageJson; 48 | res.json({ name, description }); 49 | }); 50 | 51 | router.get('/uptime', requireAdminAuthMiddleware, (req, res) => { 52 | const { name, version } = packageJson; 53 | const uptime = Date.now() - uptime_start; 54 | const ret = { 55 | name, 56 | version, 57 | node_version: process.version, 58 | hostname: os.hostname(), 59 | uptime: Math.floor(uptime / 1000), 60 | uptime_hr: millisecondsToStr(uptime), 61 | memory_usage: Math.round(process.memoryUsage().rss / 1024 / 1024) + 'M' 62 | }; 63 | res.json(ret); 64 | }); 65 | 66 | // /authorized_keys 67 | router.use('/authorized_keys', handleKeys(express)); 68 | 69 | // Groups 70 | router.use('/groups', handleGroups(express)); 71 | 72 | // /accounts 73 | router.use('/accounts', handleAccounts(express)); 74 | 75 | // /permissions 76 | router.use('/permissions', handlePermissions(express)); 77 | 78 | // import/export 79 | router.use('/impexp', handleImpExp(express)); 80 | 81 | router.post('/flushdb', requireAdminAuthMiddleware, async (req, res, next) => { 82 | if (!process.env.MODE || process.env.MODE !== 'test') { 83 | res.status(403); 84 | res.json({ status: 403, reason: 'Operation is forbidden' }); 85 | return; 86 | } 87 | try { 88 | const dh = DbHelper(); 89 | const done = await dh._flush(req.db); 90 | if (done) { 91 | if (process.env.CLUSTER_MODE && process.env.CLUSTER_MODE === '1') { 92 | notifyReloadTokens(); 93 | } 94 | res.sendStatus(204); 95 | } else { 96 | res.sendStatus(404); 97 | } 98 | } catch (err) { 99 | console.error('Error while flushing db', err); 100 | res.sendStatus(500); 101 | } 102 | }); 103 | 104 | // Reload tokens, called using CORE token 105 | router.post('/tokens', requireCoreAuthMiddleware, async (req, res, next) => { 106 | try { 107 | const { tokens } = req.body; 108 | if (!tokens.admin && !tokens.admins && !tokens.clients) { 109 | res.status(400); 110 | res.json({ code: 400, reason: 'Invalid payload' }); 111 | return; 112 | } 113 | const atm = new AuthTokenManager(req.db); 114 | await atm.delete(); 115 | if (tokens.admin) { 116 | const { token, assignee } = AuthTokenManager.getTokenAssignee(tokens.admin, true); 117 | await atm.create(token, 'admin', assignee); 118 | } 119 | if (tokens.admins) { 120 | for (let i = 0; i < tokens.admins.length; i++) { 121 | const { token, assignee } = AuthTokenManager.getTokenAssignee(tokens.admins[i]); 122 | await atm.create(token, 'admin', assignee); 123 | } 124 | } 125 | if (tokens.clients) { 126 | for (let i = 0; i < tokens.clients.length; i++) { 127 | await atm.create(tokens.clients[i], 'agent'); 128 | } 129 | } 130 | if (process.env.CLUSTER_MODE && process.env.CLUSTER_MODE === '1') { 131 | notifyReloadTokens(); 132 | res.sendStatus(204); 133 | } else { 134 | const ah = AppHelper(); 135 | const tokens = await atm.getAll(); 136 | ah.reloadAuthToken(tokens); 137 | res.sendStatus(204); 138 | } 139 | } catch (err) { 140 | console.error('Error while pushing tokens', err); 141 | res.status(500); 142 | res.json({ code: 500, reason: 'Fail to update tokens' }); 143 | } 144 | }); 145 | return router; 146 | }; 147 | -------------------------------------------------------------------------------- /src/routes/keys.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { requireAuthMiddleware } from '../lib/middlewares/AuthMiddleware'; 16 | import { getAuthorizedKeys, getAuthorizedKeysAsJson } from '../lib/helpers/KeysHelper'; 17 | import { dnsReverse } from '../lib/utils/dnsUtils'; 18 | import AppHelper from '../lib/helpers/AppHelper'; 19 | import RemoteLoggerHelper from '../lib/helpers/RemoteLoggerHelper'; 20 | import { common_debug, common_error } from '../lib/utils/logUtils'; 21 | import KeyManager from '../lib/managers/KeyManager'; 22 | 23 | const checkFingerPrint = async function (user, host, fingerprint, keys) { 24 | for (let i = 0; i < keys.length; i++) { 25 | if (keys[i].fingerprint === fingerprint) { 26 | const data = { 27 | user, 28 | host, 29 | email: keys[i].email, 30 | ts: Date.now() 31 | }; 32 | RemoteLoggerHelper.log(data); 33 | return keys[i]; 34 | } 35 | } 36 | common_debug('No public key found for %s on %s with %s fingerprint', user, host, fingerprint); 37 | }; 38 | 39 | const checkUserHost = async function (dm, accept, user, host, res, fingerprint) { 40 | if (accept && accept.indexOf('application/json') >= 0) { 41 | const { keys, cache } = await getAuthorizedKeysAsJson(dm, user, host); 42 | res.header('X-From-Cache', cache); 43 | res.json(keys); 44 | if (fingerprint) { 45 | try { 46 | const key = await checkFingerPrint(user, host, fingerprint, keys); 47 | if (key) { 48 | setImmediate(() => { 49 | const db = dm.getClient('rw'); 50 | db.open() 51 | .then(() => { 52 | return KeyManager.setLastUsed(db, fingerprint); 53 | }) 54 | .catch(e => { 55 | console.error('Failed to update last_used_at', e); 56 | }) 57 | .finally(() => { 58 | const ret = db.close(); 59 | if (ret && ret instanceof Promise) { 60 | ret.catch(err => console.error(err)); 61 | } 62 | }); 63 | }); 64 | } 65 | } catch (e) {} 66 | } 67 | } else { 68 | const ah = AppHelper(); 69 | const settingsKeys = ah.getSettings('keys'); 70 | if (settingsKeys && settingsKeys.sign) { 71 | const err = new Error('Not Acceptable when FORCE_SIGNED_KEY is true'); 72 | err.t_code = 406; 73 | throw err; 74 | } 75 | const { keys, cache } = await getAuthorizedKeys(dm, user, host); 76 | res.header('X-From-Cache', cache); 77 | res.header('Content-Type', 'text/plain'); 78 | res.send(keys); 79 | } 80 | }; 81 | 82 | export default function handleKeys(express) { 83 | const router = express.Router(); 84 | 85 | router.get('/:host/:user', requireAuthMiddleware, async (req, res) => { 86 | const accept = req.header('Accept'); 87 | const { host, user } = req.params; 88 | const { f } = req.query; 89 | try { 90 | await checkUserHost(req.dm, accept, user, host, res, f); 91 | } catch (err) { 92 | if (process.env.MODE === 'test') { 93 | common_error('Failed authorized_keys', err.message); 94 | } 95 | res.status(err.t_code || 500); 96 | res.json({ status: err.t_code || 500, reason: err.message }); 97 | console.error(err); 98 | } 99 | }); 100 | 101 | router.get('/:user', requireAuthMiddleware, async (req, res) => { 102 | const accept = req.header('Accept'); 103 | const { user } = req.params; 104 | const { f } = req.query; 105 | const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 106 | try { 107 | const host = await dnsReverse(ip); 108 | if (host && host.length > 0) { 109 | try { 110 | await checkUserHost(req.dm, accept, user, host[0], res, f); 111 | } catch (err) { 112 | if (process.env.MODE === 'test') { 113 | common_error('Failed authorized_keys', err.message); 114 | } 115 | res.status(err.t_code || 500); 116 | res.json({ status: err.t_code || 500, reason: err.message }); 117 | } 118 | } else { 119 | res.status(400); 120 | res.json({ status: 400, reason: 'Unable to get hostname for ' + ip }); 121 | } 122 | } catch (err) { 123 | if (process.env.MODE === 'test') { 124 | common_error('Failed reverse %s', ip, err.message); 125 | } 126 | res.status(400); 127 | res.json({ status: 400, reason: 'Unable to get hostname for ' + ip }); 128 | } 129 | }); 130 | return router; 131 | } 132 | -------------------------------------------------------------------------------- /src/routes/permissions.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { requireAdminAuthMiddleware } from '../lib/middlewares/AuthMiddleware'; 16 | import { getAuthorizedKeysAsFullJson } from '../lib/helpers/KeysHelper'; 17 | 18 | export default function handlePermissions(express) { 19 | const router = express.Router(); 20 | router.get('/:host/:user', requireAdminAuthMiddleware, async (req, res) => { 21 | const { host, user } = req.params; 22 | try { 23 | const keys = await getAuthorizedKeysAsFullJson(req.db, user, host); 24 | res.json(keys); 25 | } catch (err) { 26 | if (process.env.MODE === 'test') { 27 | console.error('Failed search permissions', err); 28 | } 29 | res.status(err.t_code || 500); 30 | res.json({ status: err.t_code || 500, reason: err.message }); 31 | } 32 | }); 33 | return router; 34 | } 35 | -------------------------------------------------------------------------------- /src/theoserver.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Microservice from '@authkeys/microservice'; 16 | import { initRoutes } from './routes'; 17 | import packageJson from '../package.json'; 18 | import { authMiddleware } from './lib/middlewares/AuthMiddleware'; 19 | import { auditMiddleware } from './lib/middlewares/AuditMiddleware'; 20 | import { common_error, common_warn } from './lib/utils/logUtils'; 21 | import UpdateHelper from './lib/helpers/UpdateHelper'; 22 | 23 | const noDbUrls = [{ path: '/authorized_keys', partial: true }, { path: '/' }, { path: '/uptime' }]; 24 | 25 | let last_check = 0; 26 | 27 | const CHECK_INTERVAL = 604800000; // 7days: 7 * 24 * 60 * 60 * 1000; 28 | 29 | const doURLneedDb = function (path) { 30 | for (let i = 0; i < noDbUrls.length; i++) { 31 | if (noDbUrls[i].partial) { 32 | if (path.indexOf(noDbUrls[i].path) === 0) { 33 | return false; 34 | } 35 | } else { 36 | if (path === noDbUrls[i]) { 37 | return false; 38 | } 39 | } 40 | } 41 | return true; 42 | }; 43 | 44 | class TheoServer extends Microservice { 45 | dm; 46 | 47 | constructor(environment, dm) { 48 | super(environment); 49 | this.dm = dm; 50 | this.skip_updatecheck = this.envBool(process.env, 'SKIP_UPDATECHECK', false); 51 | if (!this.skip_updatecheck) { 52 | last_check = Date.now(); 53 | UpdateHelper.checkUpdate(true); 54 | } else { 55 | common_warn(' SKIP_UPDATECHECK'); 56 | common_warn(' we will not check for new versions!'); 57 | common_warn(' be sure to star https://github.com/theoapp/theo-node'); 58 | common_warn(' to be notified when a new release is out!'); 59 | } 60 | } 61 | 62 | setupRoutes(app, express) { 63 | app.disable('x-powered-by'); 64 | app.use(authMiddleware); 65 | app.use(auditMiddleware); 66 | app.use(async (req, res, next) => { 67 | const { path, method } = req; 68 | res.set('Connection', 'close'); 69 | req.dm = this.dm; 70 | if (method !== 'HEAD') { 71 | const needDb = doURLneedDb(path); 72 | if (needDb) { 73 | const client = this.dm.getClient(method === 'GET' ? 'ro' : 'rw'); 74 | try { 75 | await client.open(); 76 | } catch (err) { 77 | common_error('Failed to open db:', err.message); 78 | // Ops.. 79 | res.status(500); 80 | res.json({ status: 500, reason: 'A problem occurred, please retry' }); 81 | return; 82 | } 83 | req.db = client; 84 | res.on('finish', () => { 85 | try { 86 | client.close(); 87 | } catch (e) { 88 | common_error('failed to close db:', e.message); 89 | } 90 | }); 91 | } 92 | } 93 | if (!this.skip_updatecheck) { 94 | res.on('finish', () => { 95 | if (Date.now() - last_check > CHECK_INTERVAL) { 96 | last_check = Date.now(); 97 | UpdateHelper.checkUpdate(); 98 | } 99 | }); 100 | } 101 | next(); 102 | }); 103 | app.use('/', initRoutes(express)); 104 | } 105 | 106 | getName() { 107 | return packageJson.name; 108 | } 109 | 110 | getVersion() { 111 | return packageJson.version; 112 | } 113 | } 114 | 115 | export default TheoServer; 116 | -------------------------------------------------------------------------------- /tests/Mocks.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const mockDateNow = function () { 16 | // mock now = 1462361249717ms = 4th May 2016 11:27:29,717 17 | return 1462361249717; 18 | }; 19 | 20 | export const mockDateToISOString = function () { 21 | return '2016-05-04T11:27:29.717Z'; 22 | }; 23 | 24 | export const uuidMock = function () { 25 | return 'a0812c69-07c1-46ea-95b8-3027698b6f30'; 26 | }; 27 | 28 | class RedisClientMock { 29 | redisManager; 30 | 31 | constructor(redisManager) { 32 | this.redisManager = redisManager; 33 | } 34 | 35 | on(event, cb) { 36 | if (event === 'message') { 37 | // eslint-disable-next-line node/no-callback-literal 38 | return cb('test_config_messages', 'flush_tokens'); 39 | } 40 | } 41 | 42 | subscribe(channel) { 43 | this.redisManager._subscribe(channel); 44 | } 45 | 46 | publish(channel, message) { 47 | this.redisManager._publish(channel, message); 48 | } 49 | } 50 | 51 | export class RedisManagerMock { 52 | channels = {}; 53 | 54 | subscribers = {}; 55 | 56 | constructor(fail = false) { 57 | this.fail = fail; 58 | } 59 | 60 | _publish(channel, message) { 61 | if (!this.channels[channel]) { 62 | this.channels[channel] = []; 63 | } 64 | this.channels[channel].push(message); 65 | return true; 66 | } 67 | 68 | getChannelMessage(channel) { 69 | return this.channels[channel][0]; 70 | } 71 | 72 | _subscribe(channel) { 73 | if (!this.subscribers[channel]) { 74 | this.subscribers[channel] = []; 75 | } 76 | this.subscribers[channel].push(true); 77 | return true; 78 | } 79 | 80 | open(cb) { 81 | if (cb) { 82 | if (this.fail) { 83 | return cb(new Error('Fake connection error')); 84 | } 85 | return cb(null, new RedisClientMock(this)); 86 | } 87 | return Promise.resolve(new RedisClientMock(this)); 88 | } 89 | 90 | getSubscribers(channel) { 91 | return this.subscribers[channel].length; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/groups.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "admin", 4 | "permissions": [ 5 | { 6 | "user": "name", 7 | "host": "edu" 8 | } 9 | ], 10 | "accounts": [ 11 | { 12 | "email": "asiemantel1c@redcross.org" 13 | }, 14 | { 15 | "email": "scallar1b@1und1.de" 16 | } 17 | ] 18 | }, 19 | { 20 | "name": "admin2", 21 | "permissions": [ 22 | { 23 | "user": "root", 24 | "host": "xxx", 25 | "ssh_options": { 26 | "from": ["192.168.1.1"] 27 | } 28 | } 29 | ], 30 | "accounts": [ 31 | { 32 | "email": "klocal1@ezinearticles.com" 33 | }, 34 | { 35 | "email": "hantonchik3@deviantart.com" 36 | } 37 | ] 38 | }, 39 | { 40 | "name": "admin3", 41 | "permissions": [ 42 | { 43 | "user": "%", 44 | "host": "xxx", 45 | "ssh_options": { 46 | "from": ["192.168.2.1"] 47 | } 48 | } 49 | ], 50 | "accounts": [ 51 | { 52 | "email": "klocal1@ezinearticles.com" 53 | } 54 | ] 55 | }, 56 | { 57 | "name": "admin4", 58 | "permissions": [ 59 | { 60 | "user": "xxx", 61 | "host": "%", 62 | "ssh_options": { 63 | "from": ["192.168.3.1"] 64 | } 65 | } 66 | ], 67 | "accounts": [ 68 | { 69 | "email": "klocal1@ezinearticles.com" 70 | } 71 | ] 72 | }, 73 | { 74 | "name": "ansible", 75 | "permissions": [ 76 | { 77 | "user": "ansibleusr", 78 | "host": "%", 79 | "ssh_options": { 80 | "from": ["192.168.3.1"] 81 | } 82 | } 83 | ], 84 | "accounts": [ 85 | { 86 | "email": "ansible1@newsvine.com" 87 | }, 88 | { 89 | "email": "ansible2@newsvine.com" 90 | } 91 | ] 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /tests/testCoreOnRestart.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import dotenv from 'dotenv'; 16 | import { describe, it } from 'mocha'; 17 | import assert from 'assert'; 18 | import fetch from 'node-fetch'; 19 | 20 | import { tokensOne, tokensTwo, tokensThree } from './testCoreTokens'; 21 | 22 | dotenv.config(); 23 | const base_url = process.env.THEO_URL || 'http://localhost:9100'; 24 | 25 | describe('Core', function () { 26 | describe('Recheck tokens after restart', function () { 27 | it('should return 401', async function () { 28 | const res = await fetch(base_url + '/tokens', { 29 | method: 'POST', 30 | headers: { 31 | Authorization: 'Bearer ' + tokensOne.tokens.admins[0].token, 32 | 'Content-Type': 'application/json; charset=utf-8' 33 | }, 34 | body: JSON.stringify(tokensOne) 35 | }); 36 | assert.strictEqual(res.status, 401); 37 | }); 38 | 39 | it('should return 401', async function () { 40 | const res = await fetch(base_url + '/accounts', { 41 | method: 'GET', 42 | headers: { 43 | Authorization: 'Bearer ' + tokensOne.tokens.admins[0].token 44 | } 45 | }); 46 | assert.strictEqual(res.status, 401); 47 | }); 48 | 49 | it('should return 401', async function () { 50 | const res = await fetch(base_url + '/accounts', { 51 | method: 'GET', 52 | headers: { 53 | Authorization: 'Bearer ' + tokensTwo.tokens.clients[0] 54 | } 55 | }); 56 | assert.strictEqual(res.status, 401); 57 | }); 58 | 59 | it('should return 401', async function () { 60 | const res = await fetch(base_url + '/authorized_keys/host/user', { 61 | method: 'GET', 62 | headers: { 63 | Authorization: 'Bearer ' + tokensOne.tokens.admins[0].token 64 | } 65 | }); 66 | assert.strictEqual(res.status, 401); 67 | }); 68 | 69 | it('should return 401', async function () { 70 | const res = await fetch(base_url + '/authorized_keys/host/user', { 71 | method: 'GET', 72 | headers: { 73 | Authorization: 'Bearer ' + tokensTwo.tokens.admins[0].token 74 | } 75 | }); 76 | assert.strictEqual(res.status, 401); 77 | }); 78 | 79 | it('should return 401', async function () { 80 | const res = await fetch(base_url + '/authorized_keys/host/user', { 81 | method: 'GET', 82 | headers: { 83 | Authorization: 'Bearer ' + tokensTwo.tokens.clients[0] 84 | } 85 | }); 86 | assert.strictEqual(res.status, 401); 87 | }); 88 | 89 | it('should return 401', async function () { 90 | const res = await fetch(base_url + '/authorized_keys/host/user', { 91 | method: 'GET', 92 | headers: { 93 | Authorization: 'Bearer ' + tokensTwo.tokens.clients[1] 94 | } 95 | }); 96 | assert.strictEqual(res.status, 401); 97 | }); 98 | 99 | it('should return 200', async function () { 100 | const res = await fetch(base_url + '/authorized_keys/host/user', { 101 | method: 'GET', 102 | headers: { 103 | Authorization: 'Bearer ' + tokensThree.tokens.admins[0].token 104 | } 105 | }); 106 | assert.strictEqual(res.status, 200); 107 | }); 108 | 109 | it('should return 200', async function () { 110 | const res = await fetch(base_url + '/authorized_keys/host/user', { 111 | method: 'GET', 112 | headers: { 113 | Authorization: 'Bearer ' + tokensThree.tokens.admins[1].token 114 | } 115 | }); 116 | assert.strictEqual(res.status, 200); 117 | }); 118 | 119 | it('should return 200', async function () { 120 | const res = await fetch(base_url + '/authorized_keys/host/user', { 121 | method: 'GET', 122 | headers: { 123 | Authorization: 'Bearer ' + tokensThree.tokens.clients[0] 124 | } 125 | }); 126 | assert.strictEqual(res.status, 200); 127 | }); 128 | 129 | it('should return 200', async function () { 130 | const res = await fetch(base_url + '/authorized_keys/host/user', { 131 | method: 'GET', 132 | headers: { 133 | Authorization: 'Bearer ' + tokensThree.tokens.clients[1] 134 | } 135 | }); 136 | assert.strictEqual(res.status, 200); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /tests/testCoreOnRestartNoAssignee.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import dotenv from 'dotenv'; 16 | import { describe, it } from 'mocha'; 17 | import assert from 'assert'; 18 | import fetch from 'node-fetch'; 19 | 20 | import { tokensOne, tokensTwo, tokensThree } from './testCoreTokensNoAssignee'; 21 | 22 | dotenv.config(); 23 | const base_url = process.env.THEO_URL || 'http://localhost:9100'; 24 | 25 | describe('Core', function () { 26 | describe('Recheck tokens after restart', function () { 27 | it('should return 401', async function () { 28 | const res = await fetch(base_url + '/tokens', { 29 | method: 'POST', 30 | headers: { 31 | Authorization: 'Bearer ' + tokensOne.tokens.admin, 32 | 'Content-Type': 'application/json; charset=utf-8' 33 | }, 34 | body: JSON.stringify(tokensOne) 35 | }); 36 | assert.strictEqual(res.status, 401); 37 | }); 38 | 39 | it('should return 401', async function () { 40 | const res = await fetch(base_url + '/accounts', { 41 | method: 'GET', 42 | headers: { 43 | Authorization: 'Bearer ' + tokensOne.tokens.admin 44 | } 45 | }); 46 | assert.strictEqual(res.status, 401); 47 | }); 48 | 49 | it('should return 401', async function () { 50 | const res = await fetch(base_url + '/accounts', { 51 | method: 'GET', 52 | headers: { 53 | Authorization: 'Bearer ' + tokensTwo.tokens.clients[0] 54 | } 55 | }); 56 | assert.strictEqual(res.status, 401); 57 | }); 58 | 59 | it('should return 401', async function () { 60 | const res = await fetch(base_url + '/authorized_keys/host/user', { 61 | method: 'GET', 62 | headers: { 63 | Authorization: 'Bearer ' + tokensOne.tokens.admin 64 | } 65 | }); 66 | assert.strictEqual(res.status, 401); 67 | }); 68 | 69 | it('should return 401', async function () { 70 | const res = await fetch(base_url + '/authorized_keys/host/user', { 71 | method: 'GET', 72 | headers: { 73 | Authorization: 'Bearer ' + tokensTwo.tokens.admin 74 | } 75 | }); 76 | assert.strictEqual(res.status, 401); 77 | }); 78 | 79 | it('should return 401', async function () { 80 | const res = await fetch(base_url + '/authorized_keys/host/user', { 81 | method: 'GET', 82 | headers: { 83 | Authorization: 'Bearer ' + tokensTwo.tokens.clients[0] 84 | } 85 | }); 86 | assert.strictEqual(res.status, 401); 87 | }); 88 | 89 | it('should return 401', async function () { 90 | const res = await fetch(base_url + '/authorized_keys/host/user', { 91 | method: 'GET', 92 | headers: { 93 | Authorization: 'Bearer ' + tokensTwo.tokens.clients[1] 94 | } 95 | }); 96 | assert.strictEqual(res.status, 401); 97 | }); 98 | 99 | it('should return 200', async function () { 100 | const res = await fetch(base_url + '/authorized_keys/host/user', { 101 | method: 'GET', 102 | headers: { 103 | Authorization: 'Bearer ' + tokensThree.tokens.admins[0] 104 | } 105 | }); 106 | assert.strictEqual(res.status, 200); 107 | }); 108 | 109 | it('should return 200', async function () { 110 | const res = await fetch(base_url + '/authorized_keys/host/user', { 111 | method: 'GET', 112 | headers: { 113 | Authorization: 'Bearer ' + tokensThree.tokens.admins[1] 114 | } 115 | }); 116 | assert.strictEqual(res.status, 200); 117 | }); 118 | 119 | it('should return 200', async function () { 120 | const res = await fetch(base_url + '/authorized_keys/host/user', { 121 | method: 'GET', 122 | headers: { 123 | Authorization: 'Bearer ' + tokensThree.tokens.clients[0] 124 | } 125 | }); 126 | assert.strictEqual(res.status, 200); 127 | }); 128 | 129 | it('should return 200', async function () { 130 | const res = await fetch(base_url + '/authorized_keys/host/user', { 131 | method: 'GET', 132 | headers: { 133 | Authorization: 'Bearer ' + tokensThree.tokens.clients[1] 134 | } 135 | }); 136 | assert.strictEqual(res.status, 200); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /tests/testCoreTokens.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const tokensOne = { 16 | tokens: { 17 | admins: [ 18 | { 19 | token: 'a1a1a1a1a1a1a1', 20 | assignee: 'admin one' 21 | } 22 | ], 23 | clients: ['ababababab', 'cdcdcdcdcdcd'] 24 | } 25 | }; 26 | 27 | export const tokensTwo = { 28 | tokens: { 29 | admins: [ 30 | { 31 | token: 'wZwZwZwZwZ', 32 | assignee: 'admin two' 33 | } 34 | ], 35 | clients: ['efefefefef', 'ghghghghghgh'] 36 | } 37 | }; 38 | 39 | export const tokensThree = { 40 | tokens: { 41 | admins: [ 42 | { 43 | token: 'q1q1q1q1q1q1q1q1', 44 | assignee: 'admin one' 45 | }, 46 | { 47 | token: 'p0p0p0p0p0p0', 48 | assignee: 'admin two' 49 | } 50 | ], 51 | clients: ['w2w2w2w2w2w2w2w2w2w2', 'e3e3e3e3e3e3e3e3e3e3e3e'] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /tests/testCoreTokensNoAssignee.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const tokensOne = { 16 | tokens: { 17 | admin: 'xYxYxY', 18 | clients: ['ababababab', 'cdcdcdcdcdcd'] 19 | } 20 | }; 21 | 22 | export const tokensTwo = { 23 | tokens: { 24 | admin: 'wZwZwZwZwZ', 25 | clients: ['efefefefef', 'ghghghghghgh'] 26 | } 27 | }; 28 | 29 | export const tokensThree = { 30 | tokens: { 31 | admins: ['q1q1q1q1q1q1q1q1', 'p0p0p0p0p0p0'], 32 | clients: ['w2w2w2w2w2w2w2w2w2w2', 'e3e3e3e3e3e3e3e3e3e3e3e'] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /tests/testStandAlone_a_HTTPUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import ServerMock from 'mock-http-server'; 17 | import { http_get, http_post } from '../src/lib/utils/httpUtils'; 18 | import { describe, it, beforeEach, afterEach } from 'mocha'; 19 | 20 | describe('Testing HTTPUtils', () => { 21 | let httpPort; 22 | const server = new ServerMock({ host: 'localhost', port: 0 }); 23 | beforeEach(function (done) { 24 | server.start(() => { 25 | httpPort = server.getHttpPort(); 26 | done(); 27 | }); 28 | }); 29 | 30 | afterEach(function (done) { 31 | server.stop(done); 32 | }); 33 | 34 | describe('Testing http_get', () => { 35 | it('Should return a json ', async () => { 36 | server.on({ 37 | method: 'GET', 38 | path: '/whatever', 39 | reply: { 40 | status: 200, 41 | headers: { 'content-type': 'application/json' }, 42 | body: '{"ret":200}' 43 | } 44 | }); 45 | const json = await http_get(`http://localhost:${httpPort}/whatever`); 46 | assert.strictEqual(json.ret, 200); 47 | }); 48 | it('Should throw a Error with code ', async () => { 49 | server.on({ 50 | method: 'GET', 51 | path: '/whatever', 52 | reply: { 53 | status: 400, 54 | headers: { 'content-type': 'application/json' }, 55 | body: '{"ret":400}' 56 | } 57 | }); 58 | try { 59 | await http_get(`http://localhost:${httpPort}/whatever`); 60 | assert.fail(); 61 | } catch (e) { 62 | assert.strictEqual(e.code, 400); 63 | } 64 | }); 65 | }); 66 | 67 | describe('Testing http_post', () => { 68 | it('Should return a json ', async () => { 69 | server.on({ 70 | method: 'POST', 71 | path: '/whatever', 72 | reply: { 73 | status: 200, 74 | headers: { 'content-type': 'application/json' }, 75 | body: '{"ret":200}' 76 | } 77 | }); 78 | const json = await http_post(`http://localhost:${httpPort}/whatever`); 79 | assert.strictEqual(json.ret, 200); 80 | }); 81 | 82 | it('Should return a json ', async () => { 83 | let bodyCheck; 84 | server.on({ 85 | method: 'POST', 86 | path: '/whatever', 87 | filter: req => { 88 | bodyCheck = req.body.test === true; 89 | return true; 90 | }, 91 | reply: { 92 | status: 200, 93 | headers: { 'content-type': 'application/json' }, 94 | body: '{"ret":200}' 95 | } 96 | }); 97 | const json = await http_post(`http://localhost:${httpPort}/whatever`, { test: true }); 98 | assert.strictEqual(json.ret, 200); 99 | assert.strictEqual(bodyCheck, true); 100 | }); 101 | 102 | it('Should return a json ', async () => { 103 | let bodyCheck = false; 104 | server.on({ 105 | method: 'POST', 106 | path: '/whatever', 107 | filter: req => { 108 | bodyCheck = req.body.test === true; 109 | return true; 110 | }, 111 | reply: { 112 | status: 200, 113 | headers: { 'content-type': 'application/json' }, 114 | body: '{"ret":200}' 115 | } 116 | }); 117 | const json = await http_post( 118 | `http://localhost:${httpPort}/whatever`, 119 | { test: true }, 120 | { 'Content-Type': 'application/json' } 121 | ); 122 | assert.strictEqual(json.ret, 200); 123 | assert.strictEqual(bodyCheck, true); 124 | }); 125 | 126 | it('Should throw a Error with code ', async () => { 127 | server.on({ 128 | method: 'POST', 129 | path: '/whatever', 130 | reply: { 131 | status: 401, 132 | headers: { 'content-type': 'application/json' }, 133 | body: '{"ret":400}' 134 | } 135 | }); 136 | try { 137 | await http_post(`http://localhost:${httpPort}/whatever`); 138 | assert.fail(); 139 | } catch (e) { 140 | assert.strictEqual(e.code, 401); 141 | } 142 | }); 143 | 144 | it('Should throw a Error ', async () => { 145 | server.on({ 146 | method: 'POST', 147 | path: '/whatever', 148 | reply: { 149 | status: 401, 150 | headers: { 'content-type': 'application/json' }, 151 | body: '{"ret":400}' 152 | } 153 | }); 154 | try { 155 | await http_post(`http://localhost:${httpPort}/whatever`, 'fake-json'); 156 | assert.fail(); 157 | } catch (e) { 158 | assert(e instanceof Error); 159 | } 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /tests/testStandAlone_a_cache.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import RedisManager from '../src/lib/cache/redis'; 17 | import MemcachedManager from '../src/lib/cache/memcached'; 18 | import { getCacheModule } from '../src/lib/cache/modules'; 19 | import { describe, it } from 'mocha'; 20 | 21 | describe('Testing cache', () => { 22 | describe('Testing getCacheModule', () => { 23 | it('Should return the RedisManager class ', () => { 24 | const clazz = getCacheModule('redis'); 25 | assert(clazz === RedisManager); 26 | }); 27 | 28 | it('Should return the Memcached class ', () => { 29 | const clazz = getCacheModule('memcached'); 30 | assert(clazz === MemcachedManager); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/testStandAlone_a_cryptoUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import { md5, sha256 } from '../src/lib/utils/cryptoUtils'; 17 | import { describe, it } from 'mocha'; 18 | 19 | const hashes = { 20 | md5: [ 21 | { 22 | string: 'ciao', 23 | hash: '6e6bc4e49dd477ebc98ef4046c067b5f' 24 | }, 25 | { 26 | string: 'not so very long sentence', 27 | hash: 'ff8848ba4fcbea3b321ed15e34bf6b62' 28 | } 29 | ], 30 | sha256: [ 31 | { 32 | string: 'ciao', 33 | hash: 'b133a0c0e9bee3be20163d2ad31d6248db292aa6dcb1ee087a2aa50e0fc75ae2' 34 | }, 35 | { 36 | string: 'not so very long sentence', 37 | hash: 'a99c57a961087361bbcd1c89f4667d3d81675eea30527c0f3cfcf81f4a8b53db' 38 | } 39 | ] 40 | }; 41 | 42 | describe('Testing cryptoUtils', () => { 43 | describe('Testing md5', () => { 44 | hashes.md5.forEach(item => { 45 | it('Should return the correct md5 hex hash for ' + item.string, () => { 46 | assert.strictEqual(md5(item.string), item.hash); 47 | }); 48 | }); 49 | }); 50 | describe('Testing sha256', () => { 51 | hashes.sha256.forEach(item => { 52 | it('Should return the correct md5 hex hash for ' + item.string, () => { 53 | assert.strictEqual(sha256(item.string), item.hash); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/testStandAlone_a_dateUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import { getTimestampFromISO8601, millisecondsToStr } from '../src/lib/utils/dateUtils'; 17 | import { describe, it } from 'mocha'; 18 | 19 | const dates = [ 20 | { 21 | str: '', 22 | ts: 0 23 | }, 24 | { 25 | str: '2019-09-22T16:47:06Z', 26 | ts: 1569170826000 27 | }, 28 | { 29 | ts: 1263170826123, 30 | str: '2010-01-11T00:47:06.123Z' 31 | } 32 | ]; 33 | 34 | const mills = [ 35 | { 36 | ms: 800, 37 | str: 'less than a second' 38 | }, 39 | { 40 | ms: 1300, 41 | str: '1 second' 42 | }, 43 | { 44 | ms: 60000, 45 | str: '1 minute' 46 | }, 47 | { 48 | ms: 120000, 49 | str: '2 minutes' 50 | }, 51 | { 52 | ms: 5400, 53 | str: '5 seconds' 54 | }, 55 | { 56 | ms: 3600000, 57 | str: '1 hour' 58 | }, 59 | { 60 | ms: 7300000, 61 | str: '2 hours' 62 | }, 63 | { 64 | ms: 3600000 * 36, 65 | str: '1 day' 66 | }, 67 | { 68 | ms: 86400000 * 62, 69 | str: '62 days' 70 | }, 71 | { 72 | ms: 86400000 * 365, 73 | str: '1 year' 74 | } 75 | ]; 76 | 77 | describe('Testing dateUtils', () => { 78 | describe('Testing getTimestampFromISO8601', () => { 79 | dates.forEach(item => { 80 | it('Should return the correct timestamp for string ' + item.str, () => { 81 | assert.strictEqual(getTimestampFromISO8601(item.str), item.ts); 82 | }); 83 | }); 84 | dates.forEach(item => { 85 | it('Should return the correct timestamp for Date ' + item.str, () => { 86 | assert.strictEqual(getTimestampFromISO8601(new Date(item.ts)), item.ts); 87 | }); 88 | }); 89 | it('Should throw an error for invalid string', () => { 90 | assert.throws(() => { 91 | getTimestampFromISO8601('asdasdasasd'); 92 | }); 93 | }); 94 | it('Should throw an error for invalid object', () => { 95 | assert.throws(() => { 96 | getTimestampFromISO8601({ 97 | getTime: () => { 98 | return 'a'; 99 | } 100 | }); 101 | }); 102 | }); 103 | it('Should throw an error for invalid object', () => { 104 | assert.throws(() => { 105 | getTimestampFromISO8601({}); 106 | }); 107 | }); 108 | }); 109 | describe('Testing millisecondsToStr', () => { 110 | mills.forEach(item => { 111 | it('Should return the correct string for ' + item.ms, () => { 112 | assert.strictEqual(millisecondsToStr(item.ms), item.str); 113 | }); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /tests/testStandAlone_a_processUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import { setTimeoutPromise } from '../src/lib/utils/processUtils'; 17 | import { describe, it } from 'mocha'; 18 | 19 | describe('Testing processUtils', () => { 20 | describe('Testing setTimeoutPromise', () => { 21 | it('Should return a promise ', done => { 22 | const timeout = 500; 23 | const obj = setTimeoutPromise(timeout); 24 | assert(obj instanceof Promise); 25 | let checked = false; 26 | obj.then(() => { 27 | checked = true; 28 | }); 29 | assert.strictEqual(checked, false); 30 | setTimeout(() => { 31 | assert.strictEqual(checked, true); 32 | done(); 33 | }, timeout); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/testStandAlone_a_rdbms.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import { getRdbmsModule } from '../src/lib/rdbms/modules'; 17 | import SqliteManager from '../src/lib/rdbms/sqlite'; 18 | import MariadbManager from '../src/lib/rdbms/mariadb'; 19 | import { describe, it } from 'mocha'; 20 | 21 | describe('Testing rdbms', () => { 22 | describe('Testing getRdbmsModule', () => { 23 | it('Should return the SqliteManager class ', () => { 24 | const clazz = getRdbmsModule('sqlite'); 25 | 26 | assert(clazz === SqliteManager); 27 | }); 28 | 29 | it('Should return the MariadbManager class ', () => { 30 | const clazz = getRdbmsModule('mariadb'); 31 | assert(clazz === MariadbManager); 32 | }); 33 | 34 | it('Should return the MariadbManager class ', () => { 35 | const clazz = getRdbmsModule('mysql'); 36 | assert(clazz === MariadbManager); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/testUpgrade_mysql.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import MariadbManager from '../src/lib/rdbms/mariadb'; 17 | import { describe, it } from 'mocha'; 18 | 19 | describe('Testing mysql upgrade', () => { 20 | it('Should return the correct version ', async () => { 21 | const { DB_HOST, DB_USER, DB_PASSWORD, DB_NAME } = process.env; 22 | const dbManager = new MariadbManager({ 23 | config: { 24 | host: DB_HOST, 25 | user: DB_USER, 26 | password: DB_PASSWORD, 27 | database: DB_NAME 28 | } 29 | }); 30 | const client = dbManager.getClient(); 31 | await client.open(); 32 | dbManager.setClient(client); 33 | const row = await dbManager.getCurrentVersion(); 34 | client.close(); 35 | dbManager.close(); 36 | assert.strictEqual(row.value, dbManager.dbVersion); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/testUpgrade_sqlite.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 AuthKeys srl 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import assert from 'assert'; 16 | import SqliteManager from '../src/lib/rdbms/sqlite'; 17 | import { describe, it } from 'mocha'; 18 | 19 | describe('Testing sqlite upgrade', () => { 20 | it('Should return the correct version ', async () => { 21 | const dbManager = new SqliteManager({ storage: process.env.DB_STORAGE }); 22 | const client = dbManager.getClient(); 23 | dbManager.setClient(client); 24 | const row = await dbManager.getCurrentVersion(); 25 | client.close(); 26 | assert.strictEqual(row.value, dbManager.dbVersion); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /theo-logo-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theoapp/theo-node/bc3e45c1b260ceba2b473c37302619a8a4d3e898/theo-logo-500.png --------------------------------------------------------------------------------