Compare commits

...

175 commits

Author SHA1 Message Date
Lukas Metzger 28abbfa356 Add version metadata to packaging 2019-12-27 12:22:26 +01:00
Lukas Metzger 52050a4298 Fixed db layout for tests 2019-12-25 23:43:48 +01:00
Lukas Metzger 72a60a0703 Adjust database layout to default powerdns layout fix #98 2019-12-25 23:30:54 +01:00
Lukas Metzger 0b2f36dbbd Fixed spelling of shure to sure 2019-12-25 21:13:35 +01:00
Lukas Metzger 6feed021d2 Bump test database version 2019-12-25 19:10:32 +01:00
Lukas Metzger 4eed13e475 Clarify error message in setup 2019-12-25 18:49:05 +01:00
Lukas Metzger 0e53529442 Fixed sql schema for installation 2019-12-25 18:31:11 +01:00
Lukas Metzger aa2a5906f9 Fixed deletion of password when editing user fixing #112 2019-12-25 18:17:44 +01:00
dependabot[bot] 36b6bf5b88 Bump axios from 0.18.0 to 0.18.1 in /backend/test (#116)
Bumps [axios](https://github.com/axios/axios) from 0.18.0 to 0.18.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.18.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.0...v0.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-25 18:09:44 +01:00
dependabot[bot] 972d03b005 Bump axios from 0.18.0 to 0.18.1 in /frontend (#115)
Bumps [axios](https://github.com/axios/axios) from 0.18.0 to 0.18.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.18.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.0...v0.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-25 17:58:59 +01:00
Lukas Metzger 2bae2042a2 Merge branch 'master' of github.com:loewexy/pdnsmanager 2019-12-25 15:31:16 +01:00
Lukas Metzger 600c7acdc1 Fixed client ip error on X-Forwarded-For 2019-12-25 15:25:39 +01:00
Virtual Mirai 2628d7f938 Fixed typo line 5 (#92) 2019-02-06 12:25:46 +01:00
GAZDOWN 091f27e9e5 Fix broken login after upgrade (#80)
* Added users(backend) field update

* dbVersion bumped to 6
2018-09-15 12:21:24 +02:00
Lukas Metzger 328d645838 Fixed errors in README.md 2018-08-27 15:05:58 +02:00
Lukas Metzger 67bff0d6bf Updated some dependencies 2018-08-27 15:00:49 +02:00
Lukas Metzger 99ad1ef4a4 Updated angular 2018-08-27 14:42:13 +02:00
Lukas Metzger 78dc31bec8 Updated dependencies to fix security vulnerabilities 2018-08-27 14:13:07 +02:00
Lukas Metzger df99f5e237 Fixed travis config 2018-08-27 14:12:14 +02:00
Lukas Metzger 3aedfbac4b Updated README.md 2018-08-27 14:00:39 +02:00
Lukas Metzger cfbe93ba38 Removed unnecessary line 2018-08-27 13:45:53 +02:00
Lukas Metzger 160156dcb5 Fixed travis config for master 2018-08-26 21:40:11 +02:00
Lukas Metzger d7430923f4 Merge branch 'dev' into 'master' for v2 2018-08-26 21:38:21 +02:00
Lukas Metzger d6285f92bb Fixed to early closing dropdown in searchfield 2018-07-31 14:21:12 +02:00
Lukas Metzger b38b1e9122 Added favicon 2018-06-10 21:26:19 +02:00
Lukas Metzger 93d8521b86 Added automatic deployment 2018-06-07 21:28:03 +02:00
Lukas Metzger 7cb02511ae Better error message and updated licence 2018-05-13 16:16:26 +02:00
Lukas Metzger bf9f68a450 Fixed error where domains with exactly a soa record where not found 2018-05-10 20:32:28 +02:00
Lukas Metzger 8ffda92071 Fixed type of second password field in setup 2018-05-10 20:10:57 +02:00
Lukas Metzger de80e3797b Fixed bug where setup stored hard coded config to config file 2018-05-10 20:04:11 +02:00
Lukas Metzger 67d3981694 Added build package script 2018-04-29 20:50:50 +02:00
Lukas Metzger 2df4ce991a Fixed exeption handling for 403 errors 2018-04-29 19:57:14 +02:00
Lukas Metzger 8db7040211 Implemented update in ui 2018-04-29 19:40:57 +02:00
Lukas Metzger b77c8232f9 Added Update APIs 2018-04-29 19:03:01 +02:00
Lukas Metzger aed50e530f Added POST /remote/changekey API 2018-04-29 16:47:20 +02:00
Lukas Metzger 5838f61db6 Added GET /remote/servertime API 2018-04-29 15:08:23 +02:00
Lukas Metzger 799ce1b371 Added /remote/changepw 2018-04-25 19:35:11 +02:00
Lukas Metzger d5e86f3583 Added ip to failed login log messages 2018-04-17 21:53:02 +02:00
Lukas Metzger b2cf655a88 Added GET /remote/ip endpoint 2018-04-17 21:30:44 +02:00
Lukas Metzger 16a56184b8 Added handling of session expiry 2018-04-13 17:29:20 +02:00
Lukas Metzger e6ca551641 Removed SOA from records 2018-04-13 17:13:53 +02:00
Lukas Metzger 2e4c49445f Added frontend for setup 2018-04-12 20:05:35 +02:00
Lukas Metzger 25346304f5 Fixed infinite loop if no users, no domains, or no records are present 2018-04-12 16:30:39 +02:00
Lukas Metzger 14039932fe Added setup to backend 2018-04-12 16:24:36 +02:00
Lukas Metzger 29f97e781e Implemented credential editor 2018-04-12 14:19:12 +02:00
Lukas Metzger f8da1e68a3 Fixed bug where invalid key was not recognized on update 2018-04-12 14:12:11 +02:00
Lukas Metzger b083024ab2 Added record deletion and minor behavioural improvements 2018-04-12 12:23:14 +02:00
Lukas Metzger 67c2f2d2a2 Added add line for auth record editor 2018-04-12 12:06:14 +02:00
Lukas Metzger 5208c36d8e Added first parts of auth editor 2018-04-12 09:27:56 +02:00
Lukas Metzger 5d2ef81610 Fixed bug where serial was never incremented 2018-04-12 09:22:32 +02:00
Lukas Metzger a0548c0e01 Improve styling of search component 2018-04-11 09:44:46 +02:00
Lukas Metzger 6342e48742 Added SOA editor for auth domains 2018-04-11 09:41:16 +02:00
Lukas Metzger 1fa2cec0a6 Added initialisation of permission array to prevent error in console 2018-04-11 08:56:26 +02:00
Lukas Metzger d2e4082dbc Fixed bug where renaming of user to current name failed 2018-04-11 08:44:44 +02:00
Lukas Metzger 08289e6745 Implemented route guards for better navigation 2018-04-11 08:36:26 +02:00
Lukas Metzger 10d3d8c50f Implemented user editor 2018-04-10 22:03:56 +02:00
Lukas Metzger ee72dd2620 Added /users/{userId} with basic implementation 2018-04-10 19:45:01 +02:00
Lukas Metzger 5d18531ec4 Added /users/create 2018-04-10 18:06:13 +02:00
Lukas Metzger 193b17ac19 Added /users 2018-04-10 17:17:20 +02:00
Lukas Metzger 0acd117e87 Added /domains/create/master,native 2018-04-10 16:16:11 +02:00
Lukas Metzger 11416c682f Added observable handling for data in login page 2018-04-10 14:19:24 +02:00
Lukas Metzger d5be125982 Updated undefined variable reference in error message 2018-04-10 14:06:51 +02:00
Lukas Metzger e4611dffd9 Added /domains/create/slave 2018-04-09 22:32:23 +02:00
Lukas Metzger 58bff34e4a Added add domain buttons 2018-04-09 22:09:54 +02:00
Lukas Metzger 6b78678c45 Fixed file names for style guide and event propagation when deleting
domain
2018-04-09 17:25:54 +02:00
Lukas Metzger 2bd996fe10 Added slave edit component 2018-04-09 17:13:47 +02:00
Lukas Metzger b4f6922c96 Added ability for user to change slave domain with permission in backend 2018-04-09 16:17:27 +02:00
Lukas Metzger 0701388c7e Added nicer select component 2018-04-09 14:57:35 +02:00
Lukas Metzger dd35643915 Added filtering and links to domain page 2018-04-09 13:30:04 +02:00
Lukas Metzger 43b911eb60 Added domains page 2018-04-09 11:16:06 +02:00
Lukas Metzger d1a32ec860 Made password change invisible for non native users 2018-04-08 17:08:17 +02:00
Lukas Metzger a2572a8a99 Added form reset after password change 2018-04-08 16:58:41 +02:00
Lukas Metzger 7d063fde98 Added password change page 2018-04-08 16:48:56 +02:00
Lukas Metzger 9c001d9d88 Added nicer logout and rout guards 2018-04-08 14:30:00 +02:00
Lukas Metzger 266a583a4d Added basic skelleton with login 2018-04-08 13:02:00 +02:00
Lukas Metzger 983ee5171a Removed obsolte frontend parts 2018-04-06 11:48:12 +02:00
Lukas Metzger 2f19aafa80 Implemented /users endpoint 2018-04-06 09:48:17 +02:00
Lukas Metzger 76e9f7327a Fixed bug where every record got changed by a record update 2018-04-05 17:17:59 +02:00
Lukas Metzger f48b0e8a11 Added GET /users 2018-04-05 16:23:55 +02:00
Lukas Metzger 8262d53280 Updated usernames in tests 2018-04-05 14:53:56 +02:00
Lukas Metzger 0e6a90fa8f Updated config structure for auth backends, and added config backend 2018-04-03 13:43:56 +02:00
Lukas Metzger ee1b081447 Fixed bug where remotes without records or domains were left in the db 2018-04-02 13:45:24 +02:00
Lukas Metzger 15ff44a86e Added 404 when adding credential for not existing domain 2018-04-02 13:40:23 +02:00
Lukas Metzger dcc0989d6d ADded transactions for multiple get handlers 2018-04-02 13:24:24 +02:00
Lukas Metzger 25382d0de3 Added DELETE /users/{user}/permissions/{domainId} 2018-04-01 21:20:53 +02:00
Lukas Metzger ff41604aa2 Added POST /users/{user}/permissions 2018-04-01 21:03:39 +02:00
Lukas Metzger 706011edd6 Added GET /users/{user}/permissions 2018-04-01 19:52:00 +02:00
Lukas Metzger c00263e072 Fixed some variable referencing errors 2018-04-01 16:11:47 +02:00
Lukas Metzger 02f8e48f11 Added PUT /records/{recordId}/credentials/{credentialId} 2018-04-01 16:05:24 +02:00
Lukas Metzger 22334174da Added GET /records/{recordId}/credentials/{credentialId} 2018-03-31 17:13:47 +02:00
Lukas Metzger 877e7c9e02 Added DELTE /records/{recordId}/credentials/{credentialId} 2018-03-31 15:29:31 +02:00
Lukas Metzger b9a6e5d7f9 Added POST /records/{recordId}/credentials 2018-03-31 14:59:45 +02:00
Lukas Metzger 2447d10dd2 Added more test cases for GET /records/{recordId}/credentials 2018-03-31 13:48:12 +02:00
Lukas Metzger 61430dd1d4 Added GET /records/{recordId}/credentials 2018-03-31 12:59:12 +02:00
Lukas Metzger 3c6debf882 Added get tests for user 2018-03-30 14:51:33 +02:00
Lukas Metzger 3f78bc8ea6 Added validation for domain type 2018-03-30 14:31:44 +02:00
Lukas Metzger 2f41db98e9 Implemented als CRUD operations for /records 2018-03-30 14:02:32 +02:00
Lukas Metzger 4a7f884fb6 Fixed static call of not static method 2018-03-30 10:18:54 +02:00
Lukas Metzger 51e294acc2 Travis debug info 2018-03-30 10:13:06 +02:00
Lukas Metzger 73a881a408 Fixed tests for updated database dump 2018-03-30 10:02:25 +02:00
Lukas Metzger 2ef6cdfa4b Added GET /records 2018-03-29 15:16:54 +02:00
Lukas Metzger 852d0d6007 Fixed missing type parameters for SQL bindValue calls 2018-03-27 19:51:29 +02:00
Lukas Metzger bac3fd1dfb Added GET /domains/{domainId}/soa 2018-03-26 20:14:45 +02:00
Lukas Metzger 28c0b0d08d Added PUT /domains/{domainId}/soa 2018-03-26 19:32:32 +02:00
Lukas Metzger 01cd32e27c Fixed bug where records where not deleted on domain deletion 2018-03-26 16:54:41 +02:00
Lukas Metzger 827d4d8280 Added PUT /domains/{domainId} 2018-03-24 21:05:38 +01:00
Lukas Metzger f4b06ae910 Added GET /domains/{domainId} 2018-03-24 16:37:35 +01:00
Lukas Metzger 29aa6e87f8 Added DELETE /domains/{domainId} 2018-03-24 15:42:11 +01:00
Lukas Metzger 8d24d6e6e1 Fixed test database 2018-03-24 15:18:15 +01:00
Lukas Metzger 6ec9c81c32 Fixed bug where numbers where returned as string 2018-03-24 15:09:33 +01:00
Lukas Metzger 5928203a0a Added POST /domains api 2018-03-24 14:48:49 +01:00
Lukas Metzger b38cf25a75 Added tests for GET /domains 2018-03-24 13:32:34 +01:00
Lukas Metzger f76a0ed8a8 Cleaned up repository 2018-03-23 20:17:24 +01:00
Lukas Metzger 7b7b739589 Cleared up travis debug info 2018-03-23 20:06:13 +01:00
Lukas Metzger 725d0e1591 Added apcu extension to travis 2018-03-23 20:01:09 +01:00
Lukas Metzger 49b45a0cf1 Changed loglevel and target in test 2018-03-23 19:54:07 +01:00
Lukas Metzger cd6f019dd0 Travis debug output 2018-03-23 19:50:39 +01:00
Lukas Metzger e95f520afa Added debug output for travis 2018-03-23 19:43:43 +01:00
Lukas Metzger a573a7813e Fixed typo in apcu.php 2018-03-23 18:49:22 +01:00
Lukas Metzger e39bb6d13c Fixed error in test if password is empty 2018-03-23 18:44:38 +01:00
Lukas Metzger 8c7fbf40ef Fixed permissions for travis scripts 2018-03-23 18:35:08 +01:00
Lukas Metzger c002e98156 Added travis-ci configuration 2018-03-23 18:32:09 +01:00
Lukas Metzger 3f4203fdf0 Added tests for backend 2018-03-23 18:16:39 +01:00
Lukas Metzger e543e9ceea Added possible override for config 2018-03-23 16:37:29 +01:00
Lukas Metzger 90381aec54 Fix commit hook 2018-03-23 16:37:11 +01:00
Lukas Metzger 878bfc3f6c Move everything to subfolder 2018-03-23 16:34:25 +01:00
Lukas Metzger dcb33c5c69 Added linter for php and fixed errors 2018-03-21 17:30:24 +01:00
Lukas Metzger 5f93a13412 Added GET /domains functionality 2018-03-21 16:51:14 +01:00
Lukas Metzger 54bf2a1099 Fixed identifier for backends used in the user table of the database 2018-03-21 16:49:52 +01:00
Lukas Metzger 154232236a Added parameter type for query 2018-03-21 13:43:26 +01:00
Lukas Metzger ac7f6f5b56 Created framwork for backend, working authentication and session
management
2018-03-20 10:51:47 +01:00
Lukas Metzger 1aad0d7219 Save some unknown progress 2018-02-09 16:58:04 +01:00
Maurice Meyer c3250e58fc Fixed mail handling in SOA records. Fixes #51 2017-09-24 16:51:02 +02:00
Eugen Ganshorn e0c12809e9 fixed bindColumn 2017-09-24 16:34:57 +02:00
Maurice Meyer fdaa16ea1b Merged #47. 2017-09-02 23:28:59 +02:00
Jens Meißner 00c3a32de8 Set default record type to PTR if a reverse zone is detected. 2017-08-29 12:25:37 +02:00
Jens Meißner 058cafb6d7 Keep input data after adding records to ease adding of several similar records. 2017-08-29 12:00:52 +02:00
Lukas Metzger ae8b16c825 Fixed linter errors, added pre-commit script and instructions 2017-07-23 16:52:11 +02:00
Lukas Metzger 03ae894776 Updated year in license file 2017-07-23 16:27:43 +02:00
Lukas Metzger 2511608c88 Added installation of dependencies to README 2017-07-23 16:26:09 +02:00
Lukas Metzger bff62a725d Added first version of frontend part 2017-07-23 16:24:52 +02:00
Lukas Metzger 800a832975 Updated readme for dev branch 2017-07-23 16:20:32 +02:00
Lukas Metzger fae6ac469b Remove old stuff, so that only the backend can be used as mock backend 2017-07-23 16:09:53 +02:00
Maurice Meyer 016183f244
Now displaying error when admin user can't be created 2017-07-21 17:53:45 +02:00
Maurice Meyer 4c4269c1fa Fix #34, now showing an error message 2017-07-21 17:48:53 +02:00
Maurice Meyer 31d7ecfdf3 Improved readability 2017-07-21 17:22:03 +02:00
Maurice Meyer 921b30e47d Fixed PHP Fatal error when config-user.php missing 2017-07-21 17:21:17 +02:00
Maurice Meyer f238b7edaa Moved headers to the top to fix #29 2017-07-20 16:33:20 +02:00
Maurice Meyer b1a06a8684 Removed spaces from empty lines 2017-07-20 16:22:46 +02:00
Maurice Meyer 39dd3f86c6 Moved <!DOCTYPE html> to enable setting of headers 2017-07-20 16:18:32 +02:00
Lukas Metzger 6aa5ea61df Merge branch 'master' of github.com:loewexy/pdnsmanager 2017-06-29 15:23:13 +02:00
Lukas Metzger ccc423291c Fixing possible remote code executuin vulnerability introduced by commit
3bf4e2874a

Thanks to RedTeam Pentesting for pointing out this issue
2017-06-29 15:18:45 +02:00
Jens Meißner 2bb00ea0ba Change dbPort when dbType is changed during install. (#39) 2017-05-22 11:08:05 +02:00
Emilien Devos f922f19fd3 Add powerdns 4.0 record types (#35) 2017-04-09 12:15:46 +02:00
Maurice Meyer 1d7086dc6c Added drop and readd of foreign key on user ->userid (#33)
This fixes the issue that the upgrade seems to work but the change from permissions.user to permissions.userid is not applied because of the foreign key.
2017-03-08 11:23:15 +01:00
Lukas Metzger 5c9f0da3aa Adding unique constraint on update 2017-02-19 12:52:40 +01:00
Lukas Metzger 77fa9a926d Fixed upgrade 2017-02-19 12:10:53 +01:00
Lukas Metzger a105edc4ab Fixed whitespace errors, removed unneccessary tables 2017-02-05 17:12:38 +01:00
lamclennan 107bfc7c67 Removed Slave 2017-01-08 14:55:07 +10:00
lamclennan 8f382ac72f Readme Updated 2017-01-08 14:45:56 +10:00
lamclennan 47aebceac4 Removed Slave Button 2017-01-08 14:42:59 +10:00
lamclennan 61d7ea9a51 Final Fixes 2017-01-08 14:34:35 +10:00
lamclennan d9f4b20448 Updated upgrade scripts for MySQL
Renamed the user table to users and added a unique key to it so
duplicate users can't be created.

Upgrade scripts delete duplicates and keep the lowest id (which is what
would have been used for authentication anyway, i.e. other users were
useless).

Added upgrade script to put domains names and records names to lower
case text as is required by postgres.
2017-01-08 14:15:31 +10:00
lamclennan b9efd906e5 Postgres SQL Fixes
A few commands changed to suit postgres and the "user" table.
2017-01-08 02:51:16 +10:00
lamclennan 47f3f9939e Bugs
Fixed a few code bugs. Added text to strip whitespace from records (i.e.
name, master, email)
2017-01-07 23:59:03 +10:00
lamclennan 5d5f8c4af7 Added some trimming around some VARCHAR records 2017-01-07 20:29:36 +10:00
lamclennan 7972a197ee PDO Conversion 2017-01-07 20:18:39 +10:00
lamclennan 1aaa24280f Started Work Again 2017-01-07 18:37:13 +10:00
lamclennan 3bf4e2874a PDO Conversion/Pg Support Started
Early commit.
2016-12-13 08:19:00 +10:00
Lukas Metzger 25569890ac Fixed license file 2016-08-21 11:55:48 +02:00
Lukas Metzger d9f113c95d Updated README.md, added possibility for native zones 2016-06-07 19:02:13 +02:00
Maurice Meyer 788c0d1d19 Added pagination (#19)
* Added initial functionality for pagination

* Display of pagination now works for total page number greater than 8. Note that the actual pagination is not yet implemented

* Pagination is now functional and loads the requested sites

* Added cursor pointer to pagination

* page parameter is now set to 1 if not a number or smaller than 1

* page parameter is now checked with isset to prevent errors if it is not supplied

* Changed default number of domains per page to 15

* Added isset check to $input->page to prevent errors if it is not set
2016-06-07 19:00:35 +02:00
Alexander Dormann dc8fab4d42 Implement ONLY_FULL_GROUP_BY compatibility (#15) (#16)
SQL92 requires that all columns (except aggregates) in the select clause
is part of the group by clause. SQL99 loosens this restriction a bit and
states that all columns in the select clause must be functionally
dependent of the group by clause.

This commit fixes all partial GROUP BY queries to be compatible with
SQL92. Previously, when MySQLs sql_mode included ONLY_FULL_GROUP_BY,
aggregating queries failed, resulting in an empty domain listing.
2016-05-14 12:03:06 +02:00
Lukas Metzger 0d1f5503ae Fixed regex for email address 2016-05-12 15:10:04 +02:00
Lukas Metzger c72806708b Removed unneccessary debug output 2016-05-06 18:15:15 +02:00
279 changed files with 25115 additions and 4831 deletions

13
.gitignore vendored
View file

@ -1,3 +1,10 @@
nbproject/private/
config/config-user.php
releases/
*/node_modules/*
backend/src/vendor/*
backend/src/config/ConfigUser.php
backend/src/config/ConfigOverride.php
backend/test/config.sh
backend/test/node_modules/
backend/test/logfile.log
.vscode/

36
.travis.yml Normal file
View file

@ -0,0 +1,36 @@
sudo: required
dist: trusty
language: php
services:
- mysql
php:
- "7.1"
before_install:
- sudo rm -rf ~/.nvm
- curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
- sudo apt-get install -y nodejs build-essential
install:
- .travis/frontend-install.sh
- .travis/backend-install.sh
before_script:
- phpenv config-rm xdebug.ini
- phpenv config-add .travis/data/phpconfig.ini
- mysql -e 'CREATE DATABASE pdnstest;'
- .travis/backend-start-server.sh > /dev/null 2>&1 &
script:
- .travis/frontend-lint.sh
- .travis/frontend-build.sh
- .travis/backend-lint.sh
- .travis/backend-test.sh
- .travis/build-package.sh
deploy:
- provider: script
script: .travis/deploy-snapshot.sh
skip_cleanup: true
on:
branch: master
- provider: script
script: .travis/deploy-release.sh
skip_cleanup: true
on:
tags: true

19
.travis/backend-install.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
cd backend/src
if ! composer install
then
exit 1
fi
cd ../..
cd backend/test
if ! npm install
then
exit 2
fi
cp ../../.travis/data/config-backend-test.sh config.sh
cd ../..
exit 0

5
.travis/backend-lint.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cd backend/src
composer run-script lint

View file

@ -0,0 +1,5 @@
#!/bin/bash
cd backend/src/public
php -S localhost:8000

5
.travis/backend-test.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cd backend/test
./test.sh all

13
.travis/build-package.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
if test $TRAVIS_TAG
then
utils/make-package.sh pdnsmanager-${TRAVIS_TAG:1} ${TRAVIS_TAG:1}
utils/make-package.sh pdnsmanager-$TRAVIS_COMMIT $TRAVIS_COMMIT
else
utils/make-package.sh pdnsmanager-$TRAVIS_COMMIT $TRAVIS_COMMIT
fi
exit 0

View file

@ -0,0 +1,7 @@
DBHOST="localhost"
DBUSER="root"
DBPASSWORD=""
DBNAME="pdnstest"
TESTURL="http://localhost:8000/v1"

View file

@ -0,0 +1 @@
extension="apcu.so"

3
.travis/deploy-release.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
curl -F "file=@pdnsmanager-${TRAVIS_TAG:1}.tar.gz" -u "travis:$UPLOAD_PASS" "https://upload.pdnsmanager.org/?action=release&version=${TRAVIS_TAG:1}"

3
.travis/deploy-snapshot.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
curl -F "file=@pdnsmanager-$TRAVIS_COMMIT.tar.gz" -u "travis:$UPLOAD_PASS" "https://upload.pdnsmanager.org/?action=snapshot&version=$TRAVIS_COMMIT"

5
.travis/frontend-build.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cd frontend
npm run build

5
.travis/frontend-install.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cd frontend
npm install

5
.travis/frontend-lint.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cd frontend
npm run lint

View file

@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2016-2018 Lukas Metzger
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,21 +1,18 @@
# PDNS Manager
[PDNS Manager](https://pdnsmanager.org) is a simple yet powerful free administration tool for the Powerdns authoritative nameserver. It supports master, native and slave zones.
[PDNS Manager](https://pdnsmanager.lmitsystems.de) is a simple yet powerful free administration tool for the
Powerdns authoritative nameserver. It currently only supports master zones.
PNDS Manager was developed from scratch to achieve a user-friendly
and pretty looking interface.
PDNS Manager was developed from scratch to achieve a user-friendly and pretty looking interface.
PDNS Manager also features a powerful API to set records programatically.
This can be used e.g. for a dynamic DNS service, but also to obtain certificates
from [Let's Encrypt](https://letsencrypt.org/) via the dns-01 challenge.
This can be used e.g. for a dynamic DNS service, but also to obtain certificates from [Let's Encrypt](https://letsencrypt.org/) via the dns-01 challenge.
PDNS Manager is written in PHP using [Bootstrap](http://getbootstrap.com/)
and [jQuery](http://jquery.com/). The backend uses a MySQL or Maria DB
database. The database is also used by Powerdns using the pdns-backend-mysql.
PDNS Managers Backend is written in PHP using [Slim Framework](https://www.slimframework.com/). The backend uses a MySQL/Maria DB database. The database is also used by Powerdns using the pdns-backend-mysql backend. The Frontend is based on [Angular](https://angular.io/) and [Bootstrap](https://getbootstrap.com/).
PDNS Manager also features a plugin API to support different session caches or authentication strategies. If you want to contribute a new plugin here feel free to contact me.
## More information
You can find more information and documentation as well as contact information on [pdnsmanager.lmitsystems.de](https://pdnsmanager.lmitsystems.de). There are also some tutorials to get you quickly up and running.
You can find more information and documentation as well as contact information on [pdnsmanager.org](https://pdnsmanager.org). There are also some tutorials to get you quickly up and running.
## Contribute
If you are looking for a new feature or you found a bug, feel free to create a pull request or open a issue.

View file

@ -1,103 +0,0 @@
<!DOCTYPE html>
<!--
Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<?php
require_once 'lib/headers.php';
require_once 'lib/session.php';
?>
<html>
<head>
<title>PDNS Manager - Domains</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="include/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="include/bootstrap/css/bootstrap-theme.min.css" rel="stylesheet">
<link href="include/custom.css" rel="stylesheet">
<script src="include/jquery.js"></script>
<script src="include/bootstrap/js/bootstrap.min.js"></script>
<script src="js/add-domain.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<div class="navbar-brand">
PDNS Manager
</div>
<ul class="nav navbar-nav">
<li><a href="domains.php">Domains</a></li>
<?php if($_SESSION['type'] == "admin") echo '<li><a href="users.php">Users</a></li>'; ?>
<li><a href="password.php">Password</a></li>
<li><a href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
<div class="container">
<row>
<h2 id="domain-name">Add Domain</h2>
</row>
<row>
<form>
<div class="col-md-3">
<div class="form-group">
<label for="zone-name" class="control-label">Name</label>
<input type="text" class="form-control" id="zone-name" placeholder="Name" autocomplete="off" data-regex="^([^.]+\.)*[^.]+$" tabindex="1">
</div>
<div class="form-group">
<label for="zone-primary" class="control-label">Primary</label>
<input type="text" class="form-control" id="zone-primary" placeholder="Primary" autocomplete="off" data-regex="^([^.]+\.)*[^.]+$" tabindex="2">
</div>
<div class="form-group">
<label for="zone-mail" class="control-label">Email</label>
<input type="text" class="form-control" id="zone-mail" placeholder="Email" autocomplete="off" data-regex="^.+\@.+\.[^.]+$" tabindex="3">
</div>
<button id="zone-button-add" class="btn btn-primary" tabindex="8">Add</button>
</div>
<div class="col-md-2 col-md-offset-1">
<div class="form-group">
<label for="zone-refresh" class="control-label">Refresh</label>
<input type="text" class="form-control" id="zone-refresh" placeholder="Refresh" autocomplete="off" data-regex="^[0-9]+$" tabindex="4" value="3600">
</div>
<div class="form-group">
<label for="zone-retry" class="control-label">Retry</label>
<input type="text" class="form-control" id="zone-retry" placeholder="Retry" autocomplete="off" data-regex="^[0-9]+$" tabindex="5" value="900">
</div>
</div>
<div class="col-md-2 col-md-offset-1">
<div class="form-group">
<label for="zone-expire" class="control-label">Expire</label>
<input type="text" class="form-control" id="zone-expire" placeholder="Expire" autocomplete="off" data-regex="^[0-9]+$" tabindex="6" value="604800">
</div>
<div class="form-group">
<label for="zone-ttl" class="control-label">TTL</label>
<input type="text" class="form-control" id="zone-ttl" placeholder="TTL" autocomplete="off" data-regex="^[0-9]+$" tabindex="7" value="86400">
</div>
</div>
</form>
</row>
</div>
<?php echo '<span class="hidden" id="csrfToken">' . $_SESSION['csrfToken'] . '</span>'; ?>
</body>
</html>

View file

@ -1,78 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
require_once '../lib/soa-mail.php';
$input = json_decode(file_get_contents('php://input'));
error_log($input->type);
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
if(!isset($_SESSION['type']) || $_SESSION['type'] != "admin") {
echo "Permission denied!";
exit();
}
if(isset($input->action) && $input->action == "addDomain") {
$soaData = Array();
$soaData[] = $input->primary;
$soaData[] = mail_to_soa($input->mail);
$soaData[] = date("Ymd") . "00";
$soaData[] = $input->refresh;
$soaData[] = $input->retry;
$soaData[] = $input->expire;
$soaData[] = $input->ttl;
$soaContent = implode(" ", $soaData);
$db->autocommit(false);
$stmt = $db->prepare("INSERT INTO domains(name,type) VALUES (?,?)");
$stmt->bind_param("ss", $input->name, $input->type);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("SELECT LAST_INSERT_ID()");
$stmt->execute();
$stmt->bind_result($newDomainId);
$stmt->fetch();
$stmt->close();
$stmt = $db->prepare("INSERT INTO records(domain_id,name,type,content,ttl) VALUES (?,?,'SOA',?,?)");
$stmt->bind_param("issi", $newDomainId, $input->name, $soaContent, $input->ttl);
$stmt->execute();
$stmt->close();
$db->commit();
$retval = Array();
$retval['newId'] = $newDomainId;
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,133 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
$input = json_decode(file_get_contents('php://input'));
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
if(isset($input->action) && $input->action == "getDomains") {
$sql = "
SELECT D.id,D.name,D.type,count(R.domain_id) AS records
FROM domains D
LEFT OUTER JOIN records R ON D.id = R.domain_id
LEFT OUTER JOIN permissions P ON D.id = P.domain
WHERE (P.user=? OR ?)
GROUP BY D.id
HAVING
(D.name LIKE ? OR ?) AND
(D.type=? OR ?)
";
if(isset($input->sort->field) && $input->sort->field != "") {
if($input->sort->field == "id") {
$sql .= "ORDER BY id";
} else if($input->sort->field == "name") {
$sql .= "ORDER BY name";
} else if($input->sort->field == "type") {
$sql .= "ORDER BY type";
} else if($input->sort->field == "records") {
$sql .= "ORDER BY records";
}
if(isset($input->sort->order)) {
if($input->sort->order == 0) {
$sql .= " DESC";
} else if($input->sort->order == 1) {
$sql .= " ASC";
}
}
}
$stmt = $db->prepare($sql);
if(isset($input->name)) {
$name_filter = "%" . $input->name . "%";
$name_filter_used = 0;
} else {
$name_filter = "";
$name_filter_used = 1;
}
$id_filter = $_SESSION['id'];
$id_filter_used = (int)($_SESSION['type'] == "admin" ? 1 : 0);
if(isset($input->type)) {
$type_filter = $input->type;
$type_filter_used = 0;
} else {
$type_filter = "";
$type_filter_used = 1;
}
$stmt->bind_param("sisiii",
$id_filter, $id_filter_used,
$name_filter, $name_filter_used,
$type_filter, $type_filter_used
);
$stmt->execute();
$result = $stmt->get_result();
$retval = Array();
while($obj = $result->fetch_object()) {
$retval[] = $obj;
}
}
if(isset($input->action) && $input->action == "deleteDomain") {
$domainId = $input->id;
$db->autocommit(false);
$stmt = $db->prepare("DELETE FROM permissions WHERE domain=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("DELETE FROM remote WHERE record IN (SELECT id FROM records WHERE domain_id=?)");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("DELETE FROM records WHERE domain_id=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("DELETE FROM domains WHERE id=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->close();
$db->commit();
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,286 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
require_once '../lib/soa-mail.php';
require_once '../lib/update-serial.php';
$input = json_decode(file_get_contents('php://input'));
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
//Permission check
if(isset($input->domain)) {
$permquery = $db->prepare("SELECT * FROM permissions WHERE user=? AND domain=?");
$permquery->bind_param("ii", $_SESSION['id'], $input->domain);
$permquery->execute();
$permquery->store_result();
if($permquery->num_rows() < 1 && $_SESSION['type'] != "admin") {
echo "Permission denied!";
exit();
}
} else {
echo "Permission denied!";
exit();
}
//Action for getting Records
if(isset($input->action) && $input->action == "getRecords") {
$sql = "
SELECT id,name,type,content,ttl,prio AS priority
FROM records
WHERE
(name LIKE ? OR ?) AND
(content LIKE ? OR ?) AND
(domain_id = ?) AND
(type != 'SOA')
";
if(isset($input->type)) {
$sql .= " AND type IN(";
foreach($input->type as $filtertype) {
$filtertype = $db->escape_string($filtertype);
$sql .= "'" . $filtertype . "'" . ",";
}
$sql = rtrim($sql, ",");
$sql .= ")";
}
if(isset($input->sort->field) && $input->sort->field != "") {
if($input->sort->field == "id") {
$sql .= " ORDER BY id";
} else if($input->sort->field == "name") {
$sql .= " ORDER BY name";
} else if($input->sort->field == "type") {
$sql .= " ORDER BY type";
} else if($input->sort->field == "content") {
$sql .= " ORDER BY content";
} else if($input->sort->field == "ttl") {
$sql .= " ORDER BY ttl";
} else if($input->sort->field == "priority") {
$sql .= " ORDER BY prio";
}
if(isset($input->sort->order)) {
if($input->sort->order == 0) {
$sql .= " DESC";
} else if($input->sort->order == 1) {
$sql .= " ASC";
}
}
}
$stmt = $db->prepare($sql);
if(isset($input->name)) {
$name_filter = "%" . $input->name . "%";
$name_filter_used = 0;
} else {
$name_filter = "";
$name_filter_used = 1;
}
if(isset($input->content)) {
$content_filter = "%" . $input->content . "%";
$content_filter_used = 0;
} else {
$content_filter = "";
$content_filter_used = 1;
}
$domainId = (int)$input->domain;
$stmt->bind_param("sisii",
$name_filter, $name_filter_used,
$content_filter, $content_filter_used,
$domainId
);
$stmt->execute();
$result = $stmt->get_result();
$retval = Array();
while($obj = $result->fetch_object()) {
$retval[] = $obj;
}
}
//Action for getting SOA
if(isset($input->action) && $input->action == "getSoa") {
$domainId = (int)$input->domain;
$stmt = $db->prepare("SELECT content FROM records WHERE type='SOA' AND domain_id=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->bind_result($content);
$stmt->fetch();
$content = explode(" ", $content);
$retval = Array();
$retval['primary'] = preg_replace('/\\.$/', "", $content[0]);
$retval['email'] = soa_to_mail($content[1]);
$retval['serial'] = $content[2];
$retval['refresh'] = $content[3];
$retval['retry'] = $content[4];
$retval['expire'] = $content[5];
$retval['ttl'] = $content[6];
}
//Action for getting SOA
if(isset($input->action) && $input->action == "getSerial") {
$domainId = (int)$input->domain;
$stmt = $db->prepare("SELECT content FROM records WHERE type='SOA' AND domain_id=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->bind_result($content);
$stmt->fetch();
$content = explode(" ", $content);
$retval = Array();
$retval['serial'] = $content[2];
}
//Action for saving SOA
if(isset($input->action) && $input->action == "saveSoa") {
$domainId = (int)$input->domain;
$db->autocommit(false);
$db->begin_transaction();
$stmt = $db->prepare("SELECT content FROM records WHERE type='SOA' AND domain_id=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->bind_result($content);
$stmt->fetch();
$stmt->close();
$content = explode(" ", $content);
$serial = $content[2];
$newsoa = $input->primary . " ";
$newsoa .= mail_to_soa($input->email) . " ";
$newsoa .= $serial . " ";
$newsoa .= $input->refresh . " ";
$newsoa .= $input->retry . " ";
$newsoa .= $input->expire . " ";
$newsoa .= $input->ttl;
$stmt = $db->prepare("UPDATE records SET content=?,ttl=? WHERE type='SOA' AND domain_id=?");
$stmt->bind_param("sii", $newsoa, $input->ttl, $domainId);
$stmt->execute();
$db->commit();
$retval = Array();
update_serial($db, $domainId);
}
//Action for saving Record
if(isset($input->action) && $input->action == "saveRecord") {
$domainId = $input->domain;
$stmt = $db->prepare("UPDATE records SET name=?,type=?,content=?,ttl=?,prio=? WHERE id=? AND domain_id=?");
$stmt->bind_param("sssiiii",
$input->name, $input->type,
$input->content, $input->ttl,
$input->prio,
$input->id, $domainId
);
$stmt->execute();
update_serial($db, $domainId);
}
//Action for adding Record
if(isset($input->action) && $input->action == "addRecord") {
$domainId = $input->domain;
$stmt = $db->prepare("INSERT INTO records (domain_id, name, type, content, prio, ttl) VALUES (?,?,?,?,?,?)");
$stmt->bind_param("isssii",
$domainId, $input->name,
$input->type, $input->content,
$input->prio, $input->ttl
);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("SELECT LAST_INSERT_ID()");
$stmt->execute();
$stmt->bind_result($newId);
$stmt->fetch();
$stmt->close();
$retval = Array();
$retval['newId'] = $newId;
update_serial($db, $domainId);
}
//Action for removing Record
if(isset($input->action) && $input->action == "removeRecord") {
$domainId = $input->domain;
$recordId = $input->id;
$stmt = $db->prepare("DELETE FROM records WHERE id=? AND domain_id=?");
$stmt->bind_param("ii", $recordId, $domainId);
$stmt->execute();
$stmt->close();
update_serial($db, $domainId);
}
//Action for getting domain name
if(isset($input->action) && $input->action == "getDomainName") {
$domainId = $input->domain;
$stmt = $db->prepare("SELECT name FROM domains WHERE id=?");
$stmt->bind_param("i", $domainId);
$stmt->execute();
$stmt->bind_result($domainName);
$stmt->fetch();
$stmt->close();
$retval = Array();
$retval['name'] = $domainName;
}
if (isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,137 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
$input = json_decode(file_get_contents('php://input'));
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
//Permission check
if(isset($input->record)) {
$permquery = $db->prepare("SELECT * FROM records JOIN permissions ON records.domain_id=permissions.domain WHERE user=? AND records.id=?");
$permquery->bind_param("ii", $_SESSION['id'], $input->record);
$permquery->execute();
$permquery->store_result();
if($permquery->num_rows() < 1 && $_SESSION['type'] != "admin") {
echo "Permission denied!";
exit();
}
} else {
echo "Permission denied!";
exit();
}
//Action for getting permission
if(isset($input->action) && $input->action == "getPermissions") {
$sql = "SELECT id, description, type FROM remote WHERE record=?";
$stmt = $db->prepare($sql);
$stmt->bind_param("i",$input->record);
$stmt->execute();
$result = $stmt->get_result();
$retval = Array();
while($obj = $result->fetch_object()) {
$retval[] = $obj;
}
}
//Action for adding password
if(isset($input->action) && $input->action == "addPassword") {
$passwordHash = password_hash($input->password, PASSWORD_DEFAULT);
$sql = "INSERT INTO remote(record,description,type,security) VALUES (?,?,'password',?)";
$stmt = $db->prepare($sql);
$stmt->bind_param("iss",$input->record, $input->description, $passwordHash);
$stmt->execute();
}
//Action for adding key
if(isset($input->action) && $input->action == "addKey") {
$sql = "INSERT INTO remote(record,description,type,security) VALUES (?,?,'key',?)";
$stmt = $db->prepare($sql);
$stmt->bind_param("iss",$input->record, $input->description, $input->key);
$stmt->execute();
}
//Action for updating password
if(isset($input->action) && $input->action == "changePassword") {
if(isset($input->password)) {
$passwordHash = password_hash($input->password, PASSWORD_DEFAULT);
$sql = "UPDATE remote SET description=?,security=? WHERE id=?";
$stmt = $db->prepare($sql);
$stmt->bind_param("ssi",$input->description, $passwordHash, $input->permission);
$stmt->execute();
} else {
$sql = "UPDATE remote SET description=? WHERE id=?";
$stmt = $db->prepare($sql);
$stmt->bind_param("ssi",$input->description, $input->permission);
$stmt->execute();
}
}
//Action for updating key
if(isset($input->action) && $input->action == "changeKey") {
$sql = "UPDATE remote SET description=?,security=? WHERE id=?";
$stmt = $db->prepare($sql);
$stmt->bind_param("ssi",$input->description, $input->key, $input->permission);
$stmt->execute();
}
//Action for getting key
if(isset($input->action) && $input->action == "getKey") {
$sql = "SELECT security FROM remote WHERE id=? AND type='key'";
$stmt = $db->prepare($sql);
$stmt->bind_param("i",$input->permission);
$stmt->execute();
$stmt->bind_result($key);
$stmt->fetch();
$retval = Array();
$retval['key'] = $key;
}
//Action for deleting permission
if(isset($input->action) && $input->action == "deletePermission") {
$sql = "DELETE FROM remote WHERE id=?";
$stmt = $db->prepare($sql);
$stmt->bind_param("i",$input->permission);
$stmt->execute();
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,143 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
$input = json_decode(file_get_contents('php://input'));
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
if(!isset($_SESSION['type']) || $_SESSION['type'] != "admin") {
echo "Permission denied!";
exit();
}
if(isset($input->action) && $input->action == "addUser") {
$passwordHash = password_hash($input->password, PASSWORD_DEFAULT);
$db->autocommit(false);
$stmt = $db->prepare("INSERT INTO user(name,password,type) VALUES (?,?,?)");
$stmt->bind_param("sss", $input->name, $passwordHash, $input->type);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("SELECT LAST_INSERT_ID()");
$stmt->execute();
$stmt->bind_result($newUserId);
$stmt->fetch();
$stmt->close();
$db->commit();
$retval = Array();
$retval['newId'] = $newUserId;
}
if(isset($input->action) && $input->action == "getUserData") {
$stmt = $db->prepare("SELECT name,type FROM user WHERE id=?");
$stmt->bind_param("i", $input->id);
$stmt->execute();
$stmt->bind_result($userName, $userType);
$stmt->fetch();
$stmt->close();
$retval = Array();
$retval['name'] = $userName;
$retval['type'] = $userType;
}
if(isset($input->action) && $input->action == "saveUserChanges") {
if(isset($input->password)) {
$passwordHash = password_hash($input->password, PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE user SET name=?,password=?,type=? WHERE id=?");
$stmt->bind_param("sssi", $input->name, $passwordHash, $input->type, $input->id);
$stmt->execute();
$stmt->close();
} else {
$stmt = $db->prepare("UPDATE user SET name=?,type=? WHERE id=?");
$stmt->bind_param("ssi", $input->name, $input->type, $input->id);
$stmt->execute();
$stmt->close();
}
}
if(isset($input->action) && $input->action == "getPermissions") {
$stmt = $db->prepare("
SELECT D.id,D.name
FROM permissions P
JOIN domains D ON P.domain=D.id
WHERE P.user=?
");
$stmt->bind_param("i", $input->id);
$stmt->execute();
$result = $stmt->get_result();
$retval = Array();
while($obj = $result->fetch_object()) {
$retval[] = $obj;
}
}
if(isset($input->action) && $input->action == "removePermission") {
$stmt = $db->prepare("DELETE FROM permissions WHERE user=? AND domain=?");
$stmt->bind_param("ii", $input->userId, $input->domainId);
$stmt->execute();
}
if(isset($input->action) && $input->action == "searchDomains" && isset($input->term)) {
$stmt = $db->prepare("SELECT id,name AS text FROM domains WHERE name LIKE ? AND id NOT IN(SELECT domain FROM permissions WHERE user=?)");
$searchTerm = "%" . $input->term . "%";
$stmt->bind_param("si", $searchTerm, $input->userId);
$stmt->execute();
$result = $stmt->get_result();
$retval = Array();
while($obj = $result->fetch_object()) {
$retval[] = $obj;
}
}
if(isset($input->action) && $input->action == "addPermissions") {
$stmt = $db->prepare("INSERT INTO permissions(user,domain) VALUES (?,?)");
foreach($input->domains as $domain) {
$stmt->bind_param("ii", $input->userId, $domain);
$stmt->execute();
}
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,49 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
$input = json_decode(file_get_contents('php://input'));
$sql = $db->prepare("SELECT id,password,type FROM user WHERE name=?");
$sql->bind_param("s", $input->user);
$sql->execute();
$sql->bind_result($id, $password, $type);
$sql->fetch();
if (password_verify($input->password, $password)) {
$retval['status'] = "success";
session_start();
$_SESSION['id'] = $id;
$_SESSION['type'] = $type;
$randomSecret = base64_encode(openssl_random_pseudo_bytes(32));
$_SESSION['secret'] = $randomSecret;
setcookie("authSecret", $randomSecret, 0, "/", "", false, true);
$csrfToken = base64_encode(openssl_random_pseudo_bytes(32));
$_SESSION['csrfToken'] = $csrfToken;
} else {
$retval['status'] = "fail";
}
echo json_encode($retval);

View file

@ -1,149 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
if(file_exists("../config/config-user.php")) {
echo "Permission denied!";
exit();
}
//Get input
$input = json_decode(file_get_contents('php://input'));
//Database command
$sql = "
CREATE TABLE IF NOT EXISTS domains (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
master varchar(128) DEFAULT NULL,
last_check int(11) DEFAULT NULL,
type varchar(6) NOT NULL,
notified_serial int(11) DEFAULT NULL,
account varchar(40) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY name_index (name)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS permissions (
user int(11) NOT NULL,
domain int(11) NOT NULL,
PRIMARY KEY (user,domain),
KEY domain (domain)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS records (
id int(11) NOT NULL AUTO_INCREMENT,
domain_id int(11) DEFAULT NULL,
name varchar(255) DEFAULT NULL,
type varchar(6) DEFAULT NULL,
content varchar(255) DEFAULT NULL,
ttl int(11) DEFAULT NULL,
prio int(11) NOT NULL DEFAULT '0',
change_date int(11) DEFAULT NULL,
disabled TINYINT(1) DEFAULT 0,
auth TINYINT(1) DEFAULT 1,
PRIMARY KEY (id),
KEY rec_name_index (name),
KEY nametype_index (name,type),
KEY domain_id (domain_id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE records
ADD CONSTRAINT records_ibfk_1 FOREIGN KEY (domain_id) REFERENCES domains (id) ON DELETE CASCADE;
CREATE TABLE IF NOT EXISTS user (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL,
password varchar(200) NOT NULL,
type varchar(20) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_1 FOREIGN KEY (domain) REFERENCES domains (id) ON DELETE CASCADE;
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_2 FOREIGN KEY (user) REFERENCES user (id) ON DELETE CASCADE;
CREATE TABLE IF NOT EXISTS remote (
id int(11) NOT NULL AUTO_INCREMENT,
record int(11) NOT NULL,
description varchar(255) NOT NULL,
type varchar(20) NOT NULL,
security varchar(2000) NOT NULL,
nonce varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
KEY record (record)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE remote
ADD CONSTRAINT remote_ibfk_1 FOREIGN KEY (record) REFERENCES records (id) ON DELETE CASCADE;
CREATE TABLE IF NOT EXISTS options (
name varchar(255) NOT NULL,
value varchar(2000) DEFAULT NULL,
PRIMARY KEY (name)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO options(name,value) VALUES ('schema_version', 3);
CREATE TABLE domainmetadata (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
kind VARCHAR(32),
content TEXT,
PRIMARY KEY (id)
) Engine=InnoDB;
";
$db = @new mysqli($input->host, $input->user, $input->password, $input->database, $input->port);
if($db->connect_error) {
$retval['status'] = "error";
$retval['message'] = $db->connect_error;
} else {
$passwordHash = password_hash($input->userPassword, PASSWORD_DEFAULT);
$db->multi_query($sql);
while ($db->next_result()) {;}
$stmt = $db->prepare("INSERT INTO user(name,password,type) VALUES (?,?,'admin')");
$stmt->bind_param("ss", $input->userName, $passwordHash);
$stmt->execute();
$stmt->close();
$configFile = Array();
$configFile[] = '<?php';
$configFile[] = '$config[\'db_host\'] = \'' . addslashes($input->host) . "';";
$configFile[] = '$config[\'db_user\'] = \'' . addslashes($input->user) . "';";
$configFile[] = '$config[\'db_password\'] = \'' . addslashes($input->password) . "';";
$configFile[] = '$config[\'db_name\'] = \'' . addslashes($input->database) . "';";
$configFile[] = '$config[\'db_port\'] = ' . addslashes($input->port) . ";";
file_put_contents("../config/config-user.php", implode("\n", $configFile));
$retval['status'] = "success";
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,43 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
$input = json_decode(file_get_contents('php://input'));
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
if(isset($input->action) && $input->action == "changePassword") {
$passwordHash = password_hash($input->password, PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE user SET password=? WHERE id=?");
$stmt->bind_param("si", $passwordHash, $_SESSION['id']);
$stmt->execute();
$stmt->close();
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,160 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/update-serial.php';
if(filter_input(INPUT_SERVER, "REQUEST_METHOD") == "GET") {
if(filter_input(INPUT_GET, "action") == "updateRecord") {
$input_domain = filter_input(INPUT_GET, "domain");
$input_id = filter_input(INPUT_GET, "id");
$input_password = filter_input(INPUT_GET, "password");
$input_content = filter_input(INPUT_GET, "content");
$stmt = $db->prepare("SELECT security,record FROM remote WHERE type='password' AND id=?");
$stmt->bind_param("i", $input_id);
$stmt->execute();
$stmt->bind_result($passwordHash, $record);
$stmt->fetch();
$stmt->close();
if(!password_verify($input_password, $passwordHash)) {
$return['status'] = "error";
$return['error'] = "Permission denied";
echo json_encode($return);
exit();
}
$stmt = $db->prepare("UPDATE records SET content=? WHERE name=? AND id=?");
$stmt->bind_param("ssi", $input_content, $input_domain, $record);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("SELECT domain_id FROM records WHERE id=?");
$stmt->bind_param("i",$record);
$stmt->execute();
$stmt->bind_result($domain_id);
$stmt->fetch();
$stmt->close();
update_serial($db, $domain_id);
$return['status'] = "success";
echo json_encode($return);
exit();
} else if(filter_input(INPUT_GET, "action") == "getIp") {
// If we are behind a proxy, return the first IP the request was forwarded for.
if(filter_input(INPUT_SERVER, "HTTP_X_FORWARDED_FOR") != null){
$return['ip'] = explode(",", filter_input(INPUT_SERVER, "HTTP_X_FORWARDED_FOR"))[0];
} else {
$return['ip'] = filter_input(INPUT_SERVER, "REMOTE_ADDR");
}
echo json_encode($return);
exit();
}
} else if(filter_input(INPUT_SERVER, "REQUEST_METHOD") == "POST") {
$input = json_decode(file_get_contents('php://input'));
if(isset($input->domain) && isset($input->id) && isset($input->content)) {
$stmt = $db->prepare("SELECT E.name,E.id FROM remote R JOIN records E ON R.record = E.id WHERE R.id=?");
$stmt->bind_param("i", $input->id);
$stmt->execute();
$stmt->bind_result($domainName, $record);
$stmt->fetch();
$stmt->close();
if($domainName != $input->domain) {
$return['status'] = "error";
$return['error'] = "Id and domain do not match!";
echo json_encode($return);
exit();
}
if(isset($_GET['getNonce'])) {
$newNonce = base64_encode(openssl_random_pseudo_bytes(32));
$dbNonce = $newNonce . ":" . time();
$stmt = $db->prepare("UPDATE remote SET nonce=? WHERE id=?");
$stmt->bind_param("si", $dbNonce, $input->id);
$stmt->execute();
$stmt->close();
$return['nonce'] = $newNonce;
echo json_encode($return);
exit();
} else if(isset($_GET['editRecord'])) {
$stmt = $db->prepare("SELECT security,nonce FROM remote WHERE id=?");
$stmt->bind_param("i", $input->id);
$stmt->execute();
$stmt->bind_result($pubkey, $dbNonce);
$stmt->fetch();
$stmt->close();
$nonce = explode(":", $dbNonce);
if($dbNonce == NULL || (time() - $nonce[1]) > $config['nonce_lifetime']) {
$return['status'] = "error";
$return['error'] = "No valid nonce available!";
echo json_encode($return);
exit();
}
$verifyString = $input->domain . $input->id . $input->content . $nonce[0];
$signature = base64_decode($input->signature);
if(openssl_verify($verifyString, $signature, $pubkey, OPENSSL_ALGO_SHA512) != 1) {
$return['status'] = "error";
$return['error'] = "Bad signature!";
echo json_encode($return);
exit();
}
$stmt = $db->prepare("UPDATE records SET content=? WHERE name=? AND id=?");
$stmt->bind_param("ssi", $input->content, $input->domain, $record);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("SELECT domain_id FROM records WHERE id=?");
$stmt->bind_param("i",$record);
$stmt->execute();
$stmt->bind_result($domain_id);
$stmt->fetch();
$stmt->close();
update_serial($db, $domain_id);
$return['status'] = "success";
echo json_encode($return);
exit();
} else {
$return['status'] = "error";
$return['error'] = "Wrong action";
echo json_encode($return);
exit();
}
} else {
$return['status'] = "error";
$return['error'] = "Missing data";
echo json_encode($return);
exit();
}
}

View file

@ -1,113 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/checkversion.php';
$input = json_decode(file_get_contents('php://input'));
if(isset($input->action) && $input->action == "getVersions") {
$retval['from'] = getVersion($db);
$retval['to'] = getExpectedVersion();
}
if(isset($input->action) && $input->action == "requestUpgrade") {
$currentVersion = getVersion($db);
if($currentVersion < 1) {
$sql = "
CREATE TABLE IF NOT EXISTS remote (
id int(11) NOT NULL AUTO_INCREMENT,
record int(11) NOT NULL,
description varchar(255) NOT NULL,
type varchar(20) NOT NULL,
security varchar(2000) NOT NULL,
nonce varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
KEY record (record)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `remote`
ADD CONSTRAINT `remote_ibfk_1` FOREIGN KEY (`record`) REFERENCES `records` (`id`);
CREATE TABLE IF NOT EXISTS options (
name varchar(255) NOT NULL,
value varchar(2000) DEFAULT NULL,
PRIMARY KEY (name)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO options(name,value) VALUES ('schema_version', 1);
";
$db->multi_query($sql);
while ($db->next_result()) {;}
}
if($currentVersion < 2) {
$sql = "
ALTER TABLE permissions
DROP FOREIGN KEY permissions_ibfk_1;
ALTER TABLE permissions
DROP FOREIGN KEY permissions_ibfk_2;
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_1 FOREIGN KEY (domain) REFERENCES domains (id) ON DELETE CASCADE;
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_2 FOREIGN KEY (user) REFERENCES user (id) ON DELETE CASCADE;
ALTER TABLE remote
DROP FOREIGN KEY remote_ibfk_1;
ALTER TABLE remote
ADD CONSTRAINT remote_ibfk_1 FOREIGN KEY (record) REFERENCES records (id) ON DELETE CASCADE;
ALTER TABLE records
ADD CONSTRAINT records_ibfk_1 FOREIGN KEY (domain_id) REFERENCES domains (id) ON DELETE CASCADE;
UPDATE options SET value=2 WHERE name='schema_version';
";
$db->multi_query($sql);
while ($db->next_result()) {;}
}
if($currentVersion < 3) {
$sql = "
CREATE TABLE domainmetadata (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
kind VARCHAR(32),
content TEXT,
PRIMARY KEY (id)
) Engine=InnoDB;
ALTER TABLE records ADD disabled TINYINT(1) DEFAULT 0;
ALTER TABLE records ADD auth TINYINT(1) DEFAULT 1;
UPDATE options SET value=3 WHERE name='schema_version';
";
$db->multi_query($sql);
while ($db->next_result()) {;}
}
$retval['status'] = "success";
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

View file

@ -1,118 +0,0 @@
<?php
/*
* Copyright 2016 Lukas Metzger <developer@lukas-metzger.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once '../config/config-default.php';
require_once '../lib/database.php';
require_once '../lib/session.php';
$input = json_decode(file_get_contents('php://input'));
if(!isset($input->csrfToken) || $input->csrfToken !== $_SESSION['csrfToken']) {
echo "Permission denied!";
exit();
}
if(!isset($_SESSION['type']) || $_SESSION['type'] != "admin") {
echo "Permission denied!";
exit();
}
if(isset($input->action) && $input->action == "getUsers") {
$sql = "
SELECT id,name,type
FROM user
WHERE
(name LIKE ? OR ?) AND
(type=? OR ?)
";
if(isset($input->sort->field) && $input->sort->field != "") {
if($input->sort->field == "id") {
$sql .= "ORDER BY id";
} else if($input->sort->field == "name") {
$sql .= "ORDER BY name";
} else if($input->sort->field == "type") {
$sql .= "ORDER BY type";
}
if(isset($input->sort->order)) {
if($input->sort->order == 0) {
$sql .= " DESC";
} else if($input->sort->order == 1) {
$sql .= " ASC";
}
}
}
$stmt = $db->prepare($sql);
if(isset($input->name)) {
$name_filter = "%" . $input->name . "%";
$name_filter_used = 0;
} else {
$name_filter = "";
$name_filter_used = 1;
}
if(isset($input->type)) {
$type_filter = $input->type;
$type_filter_used = 0;
} else {
$type_filter = "";
$type_filter_used = 1;
}
$stmt->bind_param("sisi",
$name_filter, $name_filter_used,
$type_filter, $type_filter_used
);
$stmt->execute();
$result = $stmt->get_result();
$retval = Array();
while($obj = $result->fetch_object()) {
$retval[] = $obj;
}
}
if(isset($input->action) && $input->action == "deleteUser") {
$userId = $input->id;
$db->autocommit(false);
$stmt = $db->prepare("DELETE FROM permissions WHERE user=?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
$stmt = $db->prepare("DELETE FROM user WHERE id=?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
$db->commit();
}
if(isset($retval)) {
echo json_encode($retval);
} else {
echo "{}";
}

23
backend/src/composer.json Normal file
View file

@ -0,0 +1,23 @@
{
"require": {
"slim/slim": "^3.9",
"monolog/monolog": "^1.23"
},
"autoload": {
"psr-4": {
"Services\\": "services/",
"Controllers\\": "controllers/",
"Operations\\": "operations/",
"Plugins\\": "plugins/",
"Middlewares\\": "middlewares/",
"Exceptions\\": "exceptions/",
"Utils\\": "utils/"
}
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.11"
},
"scripts": {
"lint": "vendor/bin/php-cs-fixer fix --dry-run --rules @PSR2,-no_trailing_whitespace_in_comment --using-cache false --stop-on-violation --diff ./"
}
}

1372
backend/src/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
<?php
$defaultConfig = [
'db' => [
'host' => 'localhost',
'user' => 'user',
'password' => 'password',
'dbname' => 'pdnsmanager',
'port' => 3306
],
'logging' => [
'level' => 'info',
'path' => ''
],
'sessionstorage' => [
'plugin' => 'apcu',
'timeout' => 3600,
'config' => null
],
'authentication' => [
'native' => [
'plugin' => 'native',
'prefix' => 'default',
'config' => null
]
],
'remote' => [
'timestampWindow' => 15
],
'records' => [
'allowedTypes' => [
'A', 'A6', 'AAAA', 'AFSDB', 'ALIAS', 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', 'DHCID',
'DLV', 'DNAME', 'DNSKEY', 'DS', 'EUI48', 'EUI64', 'HINFO',
'IPSECKEY', 'KEY', 'KX', 'LOC', 'MAILA', 'MAILB', 'MINFO', 'MR',
'MX', 'NAPTR', 'NS', 'NSEC', 'NSEC3', 'NSEC3PARAM', 'OPENPGPKEY',
'OPT', 'PTR', 'RKEY', 'RP', 'RRSIG', 'SIG', 'SPF',
'SRV', 'TKEY', 'SSHFP', 'TLSA', 'TSIG', 'TXT', 'WKS', 'MBOXFW', 'URL'
]
],
'proxys' => [],
'dbVersion' => 7
];
if (file_exists('../config/ConfigOverride.php')) {
$userConfig = require('ConfigOverride.php');
} elseif (file_exists('../config/ConfigUser.php')) {
$userConfig = require('ConfigUser.php');
} else {
return false;
}
return array('config' => array_replace_recursive($defaultConfig, $userConfig));

View file

@ -0,0 +1,177 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Credentials
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->c = $c;
}
public function getList(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('Non admin user tries to get credentials for record without permission.');
return $res->withJson(['error' => 'You have no permissions for this record.'], 403);
}
$credentials = new \Operations\Credentials($this->c);
$paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize'));
$results = $credentials->getCredentials($paging, $recordId);
return $res->withJson([
'paging' => $paging->toArray(),
'results' => $results
], 200);
}
public function postNew(Request $req, Response $res, array $args)
{
$body = $req->getParsedBody();
if (!array_key_exists('description', $body) ||
!array_key_exists('type', $body) || ($body['type'] === 'key' &&
!array_key_exists('key', $body)) || ($body['type'] === 'password' &&
!array_key_exists('password', $body))) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('User tries to add credential for record without permission.');
return $res->withJson(['error' => 'You have no permissions for the given record.'], 403);
}
$credentials = new \Operations\Credentials($this->c);
$key = array_key_exists('key', $body) ? $body['key'] : null;
$password = array_key_exists('password', $body) ? $body['password'] : null;
try {
$result = $credentials->addCredential($recordId, $body['description'], $body['type'], $key, $password);
return $res->withJson($result, 201);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('User tries to add credential with wrong type.');
return $res->withJson(['error' => 'The type is invalid.'], 400);
} catch (\Exceptions\InvalidKeyException $e) {
$this->logger->debug('User tries to add invalid credential key.');
return $res->withJson(['error' => 'The provided key is invalid.'], 400);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tries to add credential for not existing record.');
return $res->withJson(['error' => 'The provided record does not exist.'], 404);
}
}
public function delete(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$credentialId = intval($args['credentialId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('User tries to delete credential without permissions.');
return $res->withJson(['error' => 'You have no permission for this record'], 403);
}
$credentials = new \Operations\Credentials($this->c);
try {
$credentials->deleteCredential($recordId, $credentialId);
$this->logger->info('Deleted credential', ['id' => $credentialId]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No credential found for id ' . $credentialId], 404);
}
}
public function getSingle(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$credentialId = intval($args['credentialId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('Non admin user tries to get credential without permission.');
return $res->withJson(['error' => 'You have no permissions for this record.'], 403);
}
$credentials = new \Operations\Credentials($this->c);
try {
$result = $credentials->getCredential($recordId, $credentialId);
$this->logger->debug('Get credential info', ['id' => $credentialId]);
return $res->withJson($result, 200);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('Credential info not found', ['id' => $credentialId, 'record' => $recordId]);
return $res->withJson(['error' => 'No matching credential found.'], 404);
}
}
public function put(Request $req, Response $res, array $args)
{
$body = $req->getParsedBody();
if ((array_key_exists('type', $body) && $body['type'] === 'key' && !array_key_exists('key', $body))
|| (array_key_exists('type', $body) && $body['type'] === 'password' && !array_key_exists('password', $body))) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$credentialId = intval($args['credentialId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('User tries to update credential for record without permission.');
return $res->withJson(['error' => 'You have no permissions for the given record.'], 403);
}
$credentials = new \Operations\Credentials($this->c);
$key = array_key_exists('key', $body) ? $body['key'] : null;
$password = array_key_exists('password', $body) ? $body['password'] : null;
$description = array_key_exists('description', $body) ? $body['description'] : null;
$type = array_key_exists('type', $body) ? $body['type'] : null;
try {
$credentials->updateCredential($recordId, $credentialId, $description, $type, $key, $password);
return $res->withStatus(204);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('User tries to update credential with wrong type.');
return $res->withJson(['error' => 'The type is invalid.'], 400);
} catch (\Exceptions\InvalidKeyException $e) {
$this->logger->debug('User tries to update invalid credential key.');
return $res->withJson(['error' => 'The provided key is invalid.'], 400);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tries to update not existent credential.');
return $res->withJson(['error' => 'The provided credential does not exist.'], 404);
}
}
}

View file

@ -0,0 +1,227 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Domains
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->c = $c;
}
public function getList(Request $req, Response $res, array $args)
{
$domains = new \Operations\Domains($this->c);
$paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize'));
$query = $req->getQueryParam('query');
$sort = $req->getQueryParam('sort');
$type = $req->getQueryParam('type');
$userId = $req->getAttribute('userId');
$results = $domains->getDomains($paging, $userId, $query, $sort, $type);
return $res->withJson([
'paging' => $paging->toArray(),
'results' => $results
], 200);
}
public function postNew(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to add domain');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$body = $req->getParsedBody();
if (!array_key_exists('name', $body) ||
!array_key_exists('type', $body) || ($body['type'] === 'SLAVE' && !array_key_exists('master', $body))) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$name = $body['name'];
$type = $body['type'];
$master = isset($body['master']) ? $body['master'] : null;
$domains = new \Operations\Domains($this->c);
try {
$result = $domains->addDomain($name, $type, $master);
$this->logger->info('Created domain', $result);
return $res->withJson($result, 201);
} catch (\Exceptions\AlreadyExistentException $e) {
$this->logger->debug('Zone with name ' . $name . ' already exists.');
return $res->withJson(['error' => 'Zone with name ' . $name . ' already exists.'], 409);
} catch (\Exceptions\SemanticException $e) {
$this->logger->info('Invalid type for new domain', ['type' => $type]);
return $res->withJson(['error' => 'Invalid type allowed are MASTER, NATIVE and SLAVE'], 400);
}
}
public function delete(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to delete domain');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$domains = new \Operations\Domains($this->c);
$domainId = intval($args['domainId']);
try {
$domains->deleteDomain($domainId);
$this->logger->info('Deleted domain', ['id' => $domainId]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
}
}
public function getSingle(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$domainId = intval($args['domainId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $domainId)) {
$this->logger->info('Non admin user tries to get domain without permission.');
return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
}
$domains = new \Operations\Domains($this->c);
try {
$result = $domains->getDomain($domainId);
$this->logger->debug('Get domain info', ['id' => $domainId]);
return $res->withJson($result, 200);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
}
}
public function put(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$domainId = intval($args['domainId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $domainId)) {
$this->logger->info('User tries to update domain without permission');
return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
}
$body = $req->getParsedBody();
if (!array_key_exists('master', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$master = $body['master'];
$domains = new \Operations\Domains($this->c);
try {
$result = $domains->updateSlave($domainId, $master);
$this->logger->debug('Update master', ['id' => $domainId]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('Trying to update non existing slave zone', ['id' => $domainId]);
return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('Trying to update non slave zone', ['id' => $domainId]);
return $res->withJson(['error' => 'Domain is not a slave zone'], 405);
}
}
public function putSoa(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$domainId = $args['domainId'];
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $domainId)) {
$this->logger->info('Non admin user tries to get domain without permission.');
return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
}
$body = $req->getParsedBody();
if (!array_key_exists('primary', $body) ||
!array_key_exists('email', $body) ||
!array_key_exists('refresh', $body) ||
!array_key_exists('retry', $body) ||
!array_key_exists('expire', $body) ||
!array_key_exists('ttl', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$soa = new \Operations\Soa($this->c);
try {
$soa->setSoa(
intval($domainId),
$body['email'],
$body['primary'],
intval($body['refresh']),
intval($body['retry']),
intval($body['expire']),
intval($body['ttl'])
);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->warning('Trying to set soa for not existing domain.', ['domainId' => $domainId]);
return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->warning('Trying to set soa for slave domain.', ['domainId' => $domainId]);
return $res->withJson(['error' => 'SOA can not be set for slave domains'], 405);
}
}
public function getSoa(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$domainId = $args['domainId'];
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $domainId)) {
$this->logger->info('Non admin user tries to get domain without permission.');
return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
}
$soa = new \Operations\Soa($this->c);
try {
$soaArray = $soa->getSoa($domainId);
return $res->withJson($soaArray, 200);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tried to get non existing soa.', ['domainId' => $domainId]);
return $res->withJson(['error' => 'This domain has no soa record.'], 404);
}
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
class NotAllowed
{
public function __invoke(\Slim\Container $c)
{
return function ($request, $response, $methods) use ($c) {
$c->logger->warning('Method ' . $request->getMethod() . ' is not valid for ' . $request->getUri()->getPath());
return $c['response']
->withHeader('Allow', \implode(', ', $methods))
->withJson(array('error' => 'Method ' . $request->getMethod() . ' is not valid use on of ' . implode(', ', $methods)), 405);
};
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
class NotFound
{
public function __invoke(\Slim\Container $c)
{
return function ($request, $response) use ($c) {
$c->logger->warning('No valid endpoint found for: ' . $request->getUri()->getPath());
return $c['response']->withJson(array('error' => 'No valid endpoint found!'), 404);
};
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Permissions
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->c = $c;
}
public function getList(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to get permissions');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize'));
$user = intval($args['user']);
$permissions = new \Operations\Permissions($this->c);
$results = $permissions->getPermissions($paging, $user);
return $res->withJson([
'paging' => $paging->toArray(),
'results' => $results
], 200);
}
public function postNew(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to add permissions');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$body = $req->getParsedBody();
if (!array_key_exists('domainId', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$user = intval($args['user']);
$permissions = new \Operations\Permissions($this->c);
try {
$permissions->addPermission($user, $body['domainId']);
$this->logger->info('Permission was added:', ['by' => $req->getAttribute('userId'), 'user' => $user, 'domain' => $body['domainId']]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'Either domain or user were not found'], 404);
}
}
public function delete(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to add permissions');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$user = intval($args['user']);
$domainId = intval($args['domainId']);
$permissions = new \Operations\Permissions($this->c);
try {
$permissions->deletePermission($user, $domainId);
$this->logger->info('Permission was removed:', ['by' => $req->getAttribute('userId'), 'user' => $user, 'domain' => $domainId]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'Either domain or user were not found'], 404);
}
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Records
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->c = $c;
}
public function getList(Request $req, Response $res, array $args)
{
$records = new \Operations\Records($this->c);
$paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize'));
$domain = $req->getQueryParam('domain');
$queryName = $req->getQueryParam('queryName');
$type = $req->getQueryParam('type');
$queryContent = $req->getQueryParam('queryContent');
$sort = $req->getQueryParam('sort');
$userId = $req->getAttribute('userId');
$results = $records->getRecords($paging, $userId, $domain, $queryName, $type, $queryContent, $sort);
return $res->withJson([
'paging' => $paging->toArray(),
'results' => $results
], 200);
}
public function postNew(Request $req, Response $res, array $args)
{
$body = $req->getParsedBody();
if (!array_key_exists('name', $body) ||
!array_key_exists('type', $body) ||
!array_key_exists('content', $body) ||
!array_key_exists('priority', $body) ||
!array_key_exists('ttl', $body) ||
!array_key_exists('domain', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$userId = $req->getAttribute('userId');
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $body['domain'])) {
$this->logger->info('User tries to add record for domain without permission.');
return $res->withJson(['error' => 'You have no permissions for the given domain.'], 403);
}
$records = new \Operations\Records($this->c);
try {
$result = $records->addRecord($body['name'], $body['type'], $body['content'], $body['priority'], $body['ttl'], $body['domain']);
return $res->withJson($result, 201);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tries to add record for invalid domain.');
return $res->withJson(['error' => 'The domain does not exist or is neighter MASTER nor NATIVE.'], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('User tries to add record with invalid type.', ['type' => $body['type']]);
return $res->withJson(['error' => 'The provided type is invalid.'], 400);
}
}
public function delete(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('User tries to delete record without permissions.');
return $res->withJson(['error' => 'You have no permission to delete this record'], 403);
}
$records = new \Operations\Records($this->c);
try {
$records->deleteRecord($recordId);
$this->logger->info('Deleted record', ['id' => $recordId]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No record found for id ' . $recordId], 404);
}
}
public function getSingle(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('Non admin user tries to get record without permission.');
return $res->withJson(['error' => 'You have no permissions for this record.'], 403);
}
$records = new \Operations\Records($this->c);
try {
$result = $records->getRecord($recordId);
$this->logger->debug('Get record info', ['id' => $recordId]);
return $res->withJson($result, 200);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No record found for id ' . $recordId], 404);
}
}
public function put(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('Non admin user tries to update record without permission.');
return $res->withJson(['error' => 'You have no permissions for this record.'], 403);
}
$body = $req->getParsedBody();
$name = array_key_exists('name', $body) ? $body['name'] : null;
$type = array_key_exists('type', $body) ? $body['type'] : null;
$content = array_key_exists('content', $body) ? $body['content'] : null;
$priority = array_key_exists('priority', $body) ? $body['priority'] : null;
$ttl = array_key_exists('ttl', $body) ? $body['ttl'] : null;
$records = new \Operations\Records($this->c);
try {
$records->updateRecord($recordId, $name, $type, $content, $priority, $ttl);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tries to update not existing record.');
return $res->withJson(['error' => 'The record does not exist.'], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('User tries to update record with invalid type.', ['type' => $type]);
return $res->withJson(['error' => 'The provided type is invalid.'], 400);
}
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Remote
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->c = $c;
}
public function ip(Request $req, Response $res, array $args)
{
return $res->withJson([
'ip' => $req->getAttribute('clientIp')
], 200);
}
public function updatePassword(Request $req, Response $res, array $args)
{
$record = $req->getParam('record');
$content = $req->getParam('content');
$password = $req->getParam('password');
if ($record === null || $content === null || $password === null) {
return $res->withJson(['error' => 'One of the required fields is missing.'], 422);
}
$remote = new \Operations\Remote($this->c);
try {
$remote->updatePassword(intval($record), $content, $password);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tried to update non existent record via changepw api.');
return $res->withJson(['error' => 'The given record does not exist.'], 404);
} catch (\Exceptions\ForbiddenException $e) {
$this->logger->debug('User tried to update an record via changepw api with incorrect password.');
return $res->withJson(['error' => 'The provided password was invalid.'], 403);
}
$this->logger->info('Record ' . $record . ' was changed via the changepw api.');
return $res->withStatus(204);
}
public function updateKey(Request $req, Response $res, array $args)
{
$record = $req->getParsedBodyParam('record');
$content = $req->getParsedBodyParam('content');
$time = $req->getParsedBodyParam('time');
$signature = $req->getParsedBodyParam('signature');
if ($record === null || $content === null || $time === null || $signature === null) {
return $res->withJson(['error' => 'One of the required fields is missing.'], 422);
}
$remote = new \Operations\Remote($this->c);
try {
$remote->updateKey($record, $content, $time, $signature);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tried to update non existent record via changekey api.');
return $res->withJson(['error' => 'The given record does not exist.'], 404);
} catch (\Exceptions\ForbiddenException $e) {
$this->logger->debug('User tried to update an record via changekey api with incorrect signature.');
return $res->withJson(['error' => 'The provided signature was invalid.'], 403);
}
$this->logger->info('Record ' . $record . ' was changed via the changekey api.');
return $res->withStatus(204);
}
public function servertime(Request $req, Response $res, array $args)
{
return $res->withJson([
'time' => time()
], 200);
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Sessions
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $container;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->container = $c;
}
public function post(Request $req, Response $res, array $args)
{
$body = $req->getParsedBody();
if (!array_key_exists('username', $body) ||
!array_key_exists('password', $body)) {
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$userAuth = new \Operations\UserAuth($this->container);
$sessionStorage = new \Operations\Sessionstorage($this->container);
$sessionTimeout = $this->container['config']['sessionstorage']['timeout'];
try {
$userId = $userAuth->authenticate($body['username'], $body['password']);
} catch (\Exceptions\PluginNotFoundException $e) {
return $res->withJson(['error' => $e->getMessage()], 500);
}
if ($userId >= 0) {
$secret = openssl_random_pseudo_bytes(64);
$secretString = base64_encode($secret);
$secretString = rtrim(strtr($secretString, '+/', '-_'), '=');
$sessionStorage->set($secretString, $userId, $sessionTimeout);
$this->logger->info('User authenticated successfully', ['username' => $body['username']]);
return $res->withJson([
'username' => $body['username'],
'token' => $secretString
], 201);
} else {
$this->logger->info('User failed to authenticate', ['username' => $body['username'], 'ip' => $req->getAttribute('clientIp')]);
return $res->withJson(['error' => 'Username or password is invalid'], 403);
}
}
public function delete(Request $req, Response $res, array $args)
{
$sessionStorage = new \Operations\Sessionstorage($this->container);
if ($sessionStorage->exists($args['sessionId'])) {
$sessionStorage->delete($args['sessionId']);
$this->logger->info('Deleting session', ['token' => $args['sessionId']]);
return $res->withStatus(204);
} else {
$this->logger->warning('Trying to delete non existing session', ['token' => $args['sessionId']]);
return $res->withJson(['error' => 'Session not found'], 404);
}
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Setup
{
public function setup(Request $req, Response $res, array $args)
{
// Check if supplied data has all fields
$body = $req->getParsedBody();
if ($body === null) {
return $res->withJson(['error' => 'The supplied body was empty'], 400);
}
if (!array_key_exists('db', $body) || !array_key_exists('admin', $body)) {
return $res->withJson(['error' => 'One of the required fields is missing.'], 422);
}
$db = $body['db'];
$admin = $body['admin'];
if (!array_key_exists('host', $db) || !array_key_exists('user', $db) ||
!array_key_exists('password', $db) || !array_key_exists('database', $db) ||
!array_key_exists('port', $db) || !array_key_exists('name', $admin) ||
!array_key_exists('password', $admin)) {
return $res->withJson(['error' => 'One of the required fields is missing.'], 422);
}
// Check if pdo exists
if (!extension_loaded('pdo')) {
return $res->withJson(['error' => 'PDO extension is not enabled.'], 500);
}
if (!extension_loaded('pdo_mysql')) {
return $res->withJson(['error' => 'PDO mysql extension is not enabled.'], 500);
}
// Check if apcu exists
if (!extension_loaded('apcu')) {
return $res->withJson(['error' => 'APCU extension is not enabled.'], 500);
}
try {
// Test database connection
$pdo = new \PDO(
'mysql:host=' . $db['host'] . ';port=' . $db['port'] . ';dbname=' . $db['database'],
$db['user'],
$db['password']
);
// Configure db connection
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
// Check if database is empty
$query = $pdo->prepare('SHOW TABLES');
$query->execute();
if ($query->fetch() !== false) {
return $res->withJson(['error' => 'The database is not empty.'], 500);
}
// Check if config can be written
if (file_put_contents('../config/ConfigUser.php', 'test') === false) {
return $res->withJson(['error' => 'Write of config file failed, check that the PHP user can write in the config directory.'], 500);
} else {
unlink('../config/ConfigUser.php');
}
// Execute sql from setup file
$sqlLines = explode(';', file_get_contents('../sql/setup.sql'));
foreach ($sqlLines as $sql) {
if (strlen(preg_replace('/\s+/', '', $sql)) > 0) {
$pdo->exec($sql);
}
}
// Create admin user
$query = $pdo->prepare('INSERT INTO users (name, backend, type, password) VALUES (:name, :backend, :type, :password)');
$query->bindValue(':name', $admin['name']);
$query->bindValue(':backend', 'native');
$query->bindValue(':type', 'admin');
$query->bindValue(':password', password_hash($admin['password'], PASSWORD_DEFAULT));
$query->execute();
// Save config file
$config = [
'db' => [
'host' => $db['host'],
'user' => $db['user'],
'password' => $db['password'],
'dbname' => $db['database'],
'port' => intval($db['port'])
]
];
$configFile = '<?php' . "\n\n" . 'return ' . var_export($config, true) . ';';
file_put_contents('../config/ConfigUser.php', $configFile);
} catch (\PDOException $e) {
return $res->withJson(['error' => $e->getMessage()], 500);
}
return $res->withStatus(204);
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Update
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
/** @var \PDO */
private $db;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
public function get(Request $req, Response $res, array $args)
{
$currentVersion = $this->getCurrentVersion();
$targetVersion = $this->c['config']['dbVersion'];
if ($currentVersion < $targetVersion) {
return $res->withJson([
'updateRequired' => true,
'currentVersion' => $currentVersion,
'targetVersion' => $targetVersion
], 200);
} else {
return $res->withJson(['updateRequired' => false], 200);
}
}
public function post(Request $req, Response $res, array $args)
{
$currentVersion = $this->getCurrentVersion();
$targetVersion = $this->c['config']['dbVersion'];
if ($currentVersion < $targetVersion) {
try {
for ($i = $currentVersion + 1; $i <= $targetVersion; $i++) {
$sqlLines = explode(';', file_get_contents('../sql/Update' . $i . '.sql'));
foreach ($sqlLines as $sql) {
if (strlen(preg_replace('/\s+/', '', $sql)) > 0) {
$this->db->exec($sql);
}
}
$this->logger->info('Upgrade to version ' . $i . ' successfull!');
}
} catch (\Exception $e) {
$this->logger->error('Upgrade failed with: ' . $e->getMessage());
return $res->withJson(['error' => $e->getMessage()], 500);
}
}
return $res->withStatus(204);
}
private function getCurrentVersion() : int
{
$query = $this->db->prepare('SHOW TABLES LIKE \'options\';');
$query->execute();
if ($query->fetch() === false) {
return 0;
}
$query = $this->db->prepare('SELECT value FROM options WHERE name=\'schema_version\'');
$query->execute();
return intval($query->fetch()['value']);
}
}

View file

@ -0,0 +1,165 @@
<?php
namespace Controllers;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Users
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->c = $c;
}
public function getList(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to get users');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$users = new \Operations\Users($this->c);
$paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize'));
$query = $req->getQueryParam('query');
$sort = $req->getQueryParam('sort');
$type = $req->getQueryParam('type');
$results = $users->getUsers($paging, $query, $type, $sort);
return $res->withJson([
'paging' => $paging->toArray(),
'results' => $results
], 200);
}
public function postNew(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to add user');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$body = $req->getParsedBody();
if (!array_key_exists('name', $body) ||
!array_key_exists('type', $body) ||
!array_key_exists('password', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$name = $body['name'];
$type = $body['type'];
$password = $body['password'];
$users = new \Operations\Users($this->c);
try {
$result = $users->addUser($name, $type, $password);
$this->logger->info('Created user', $result);
return $res->withJson($result, 201);
} catch (\Exceptions\AlreadyExistentException $e) {
$this->logger->debug('User with name ' . $name . ' already exists.');
return $res->withJson(['error' => 'User with name ' . $name . ' already exists.'], 409);
} catch (\Exceptions\SemanticException $e) {
$this->logger->info('Invalid type for new user', ['type' => $type]);
return $res->withJson(['error' => 'Invalid type allowed are admin and user'], 400);
}
}
public function delete(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if (!$ac->isAdmin($req->getAttribute('userId'))) {
$this->logger->info('Non admin user tries to delete user');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$users = new \Operations\Users($this->c);
$user = intval($args['user']);
try {
$users->deleteDomain($user);
$this->logger->info('Deleted user', ['id' => $user]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No user found for id ' . $user], 404);
}
}
public function getSingle(Request $req, Response $res, array $args)
{
$ac = new \Operations\AccessControl($this->c);
if ($args['user'] === 'me') {
$user = $req->getAttribute('userId');
} elseif ($ac->isAdmin($req->getAttribute('userId'))) {
$user = intval($args['user']);
} else {
$this->logger->info('Non admin user tries to get other user');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$users = new \Operations\Users($this->c);
try {
$result = $users->getUser($user);
$this->logger->debug('Get user info', ['id' => $user]);
return $res->withJson($result, 200);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No user found for id ' . $user], 404);
}
}
public function put(Request $req, Response $res, array $args)
{
$body = $req->getParsedBody();
$name = array_key_exists('name', $body) ? $body['name'] : null;
$type = array_key_exists('type', $body) ? $body['type'] : null;
$password = array_key_exists('password', $body) ? $body['password'] : null;
$ac = new \Operations\AccessControl($this->c);
if ($args['user'] === 'me') {
$user = $req->getAttribute('userId');
$name = null;
$type = null;
} elseif ($ac->isAdmin($req->getAttribute('userId'))) {
$user = intval($args['user']);
} else {
$this->logger->info('Non admin user tries to get other user');
return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
}
$users = new \Operations\Users($this->c);
try {
$result = $users->updateUser($user, $name, $type, $password);
$this->logger->debug('Update user', ['id' => $user]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('Trying to update non existing user', ['id' => $user]);
return $res->withJson(['error' => 'No user found for id ' . $user], 404);
} catch (\Exceptions\AlreadyExistentException $e) {
$this->logger->debug('Trying to rename user to conflicting name', ['id' => $user]);
return $res->withJson(['error' => 'The new name already exists.'], 409);
}
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Exceptions;
require '../vendor/autoload.php';
class AlreadyExistentException extends \Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
namespace Exceptions;
require '../vendor/autoload.php';
class ForbiddenException extends \Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
namespace Exceptions;
require '../vendor/autoload.php';
class InvalidKeyException extends \Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
namespace Exceptions;
require '../vendor/autoload.php';
class NotFoundException extends \Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
namespace Exceptions;
require '../vendor/autoload.php';
class PluginNotFoundException extends \Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
namespace Exceptions;
require '../vendor/autoload.php';
class SemanticException extends \Exception
{
}

View file

@ -0,0 +1,44 @@
<?php
namespace Middlewares;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class Authentication
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $container;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->container = $c;
}
public function __invoke(Request $req, Response $res, callable $next)
{
$token = $req->getHeaderLine('X-Authentication');
$sessionStorage = new \Operations\Sessionstorage($this->container);
if ($sessionStorage->exists($token)) {
$sessionTimeout = $this->container['config']['sessionstorage']['timeout'];
$userId = $sessionStorage->get($token, $sessionTimeout);
$this->logger->debug('Authentication was successfull', ['token' => $token, 'userId' => $userId]);
$req = $req->withAttribute('userId', $userId);
return $next($req, $res);
} else {
$this->logger->warning('No valid authentication token found');
return $res->withJson(['error' => 'No valid authentication token suplied', 'code' => 'invalid_session'], 403);
}
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Middlewares;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class ClientIp
{
/** @var \Monolog\Logger */
private $logger;
/** @var \Slim\Container */
private $container;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->container = $c;
}
public function __invoke(Request $req, Response $res, callable $next)
{
$proxys = $this->container['config']['proxys'];
$headerContent = $req->getHeaderLine('X-Forwarded-For');
if (strlen($headerContent) === 0) {
$ip = $_SERVER['REMOTE_ADDR'];
} else {
if (!in_array($_SERVER['REMOTE_ADDR'], $proxys)) { // Client is not trusted proxy
$ip = $_SERVER['REMOTE_ADDR'];
} else {
$parts = array_map('trim', explode(',', $headerContent));
$ip = $_SERVER['REMOTE_ADDR'];
for ($i = count($parts) - 1; $i >= 0; $i--) {
if (!in_array($parts[$i], $proxys)) {
$ip = $parts[$i];
break;
}
}
}
}
$req = $req->withAttribute('clientIp', $ip);
return $next($req, $res);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Middlewares;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class LogRequests
{
/** @var \Monolog\Logger */
private $logger;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
}
public function __invoke(Request $req, Response $res, callable $next)
{
$this->logger->debug($req->getMethod() . ' ' . $req->getUri()->getPath());
return $next($req, $res);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Middlewares;
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
class RejectEmptyBody
{
/** @var \Monolog\Logger */
private $logger;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
}
public function __invoke(Request $req, Response $res, callable $next)
{
if (($req->isPost() || $req->isPut() || $req->isPatch()) && $req->getParsedBody() == null) {
$this->logger->warning('Got empty body in request with method ' . $req->getMethod());
return $res->withJson(['error' => 'The supplied body was empty'], 400);
} else {
return $next($req, $res);
}
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides access control for the application.
*/
class AccessControl
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
}
/**
* Determines if the given user has admin privileges.
*
* @param $userId User id of the user
*
* @return bool true if admin, false otherwise
*/
public function isAdmin(int $userId) : bool
{
$query = $this->db->prepare('SELECT type FROM users WHERE id=:id');
$query->bindValue(':id', $userId, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->logger->error('Queried record for non existing user id, this should not happen.', ['userId' => $userId]);
return false;
}
return $record['type'] == 'admin';
}
/**
* Check if a given user has permissons for a given domain.
*
* @param $userId User id of the user
* @param $domainId Domain to check
*
* @return bool true if access is granted, false otherwise
*/
public function canAccessDomain(int $userId, int $domainId) : bool
{
if ($this->isAdmin($userId)) {
return true;
}
$query = $this->db->prepare('SELECT user_id,domain_id FROM permissions WHERE user_id=:userId AND domain_id=:domainId');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
return false;
} else {
return true;
}
}
/**
* Check if a given user has permissons for a given record.
*
* @param $userId User id of the user
* @param $recordId Record to check
*
* @return bool true if access is granted, false otherwise
*/
public function canAccessRecord(int $userId, int $recordId) : bool
{
if ($this->isAdmin($userId)) {
return true;
}
$query = $this->db->prepare('
SELECT * FROM records R
LEFT OUTER JOIN permissions P ON P.domain_id=R.domain_id
WHERE R.id=:recordId AND P.user_id=:userId
');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':recordId', $recordId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
return false;
} else {
return true;
}
}
}

View file

@ -0,0 +1,254 @@
<?php
namespace Operations;
use function Monolog\Handler\error_log;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying credentials.
*/
class Credentials
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of credentials
*
* @param $pi PageInfo object, which is also updated with total page number
* @param $recordId Id of the record for which the table should be retrieved
*
* @return array Array with credentials
*/
public function getCredentials(\Utils\PagingInfo &$pi, int $recordId) : array
{
$this->db->beginTransaction();
//Count elements
if ($pi->pageSize === null) {
$pi->totalPages = 1;
} else {
$query = $this->db->prepare('
SELECT COUNT(*) AS total
FROM remote
WHERE record=:recordId
');
$query->bindValue(':recordId', $recordId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
$pi->totalPages = ceil($record['total'] / $pi->pageSize);
}
$pageStr = \Services\Database::makePagingString($pi);
$query = $this->db->prepare('SELECT id,description,type FROM remote WHERE record=:recordId ORDER BY id ASC' . $pageStr);
$query->bindValue(':recordId', $recordId, \PDO::PARAM_INT);
$query->execute();
$data = $query->fetchAll();
$this->db->commit();
return array_map(function ($item) {
$item['id'] = intval($item['id']);
return $item;
}, $data);
}
/**
* Add a new credential
*
* @param $record Record for which this credential should be valid
* @param $description Description for this credential
* @param $type Type of the credential, can bei key or password
* @param $key Key if type is key, null otherwise
* @param $password Password if type was password, null otherwise
*
* @return array The new credential entry.
*/
public function addCredential(int $record, string $description, string $type, ? string $key, ? string $password) : array
{
if ($type === 'key') {
if (openssl_pkey_get_public($key) === false) {
throw new \Exceptions\InvalidKeyException();
}
$secret = $key;
} elseif ($type === 'password') {
$secret = password_hash($password, PASSWORD_DEFAULT);
} else {
throw new \Exceptions\SemanticException();
}
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM records WHERE id=:recordId');
$query->bindValue(':recordId', $record, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('INSERT INTO remote (record, description, type, security) VALUES (:record, :description, :type, :security)');
$query->bindValue(':record', $record, \PDO::PARAM_INT);
$query->bindValue(':description', $description, \PDO::PARAM_STR);
$query->bindValue(':type', $type, \PDO::PARAM_STR);
$query->bindValue(':security', $secret, \PDO::PARAM_STR);
$query->execute();
$query = $this->db->prepare('SELECT id, description, type, security FROM remote ORDER BY id DESC LIMIT 1');
$query->execute();
$record = $query->fetch();
$record['id'] = intval($record['id']);
if ($record['type'] === 'key') {
$record['key'] = $record['security'];
unset($record['security']);
} else {
unset($record['security']);
}
$this->db->commit();
return $record;
}
/**
* Delete credential
*
* @param $recordId Id of the record
* @param $credentialId Id of the credential to delete
*
* @return void
*
* @throws NotFoundException if credential does not exist
*/
public function deleteCredential(int $recordId, int $credentialId) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM remote WHERE id=:id AND record=:record');
$query->bindValue(':id', $credentialId, \PDO::PARAM_INT);
$query->bindValue(':record', $recordId, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) { //Credential does not exist
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('DELETE FROM remote WHERE id=:id');
$query->bindValue(':id', $credentialId, \PDO::PARAM_INT);
$query->execute();
$this->db->commit();
}
/**
* Get record
*
* @param $recordId Id of the record
* @param $credentialId Id of the credential
*
* @return array Credential entry
*
* @throws NotFoundException if the credential does not exist
*/
public function getCredential(int $recordId, int $credentialId) : array
{
$query = $this->db->prepare('SELECT id,description,type,security FROM remote
WHERE id=:credential AND record=:record');
$query->bindValue(':credential', $credentialId, \PDO::PARAM_INT);
$query->bindValue(':record', $recordId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
$record['id'] = intval($record['id']);
if ($record['type'] === 'key') {
$record['key'] = $record['security'];
unset($record['security']);
} else {
unset($record['security']);
}
return $record;
}
/**
* Add a new credential
*
* @param $record Record for which this credential should be valid
* @param $credential Credential to update
* @param $description Description for this credential
* @param $type Type of the credential, can bei key or password
* @param $key Key if type is key, null otherwise
* @param $password Password if type was password, null otherwise
*
* @return array The new credential entry.
*/
public function updateCredential(int $record, int $credential, ? string $description, ? string $type, ? string $key, ? string $password) : array
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,record,description,type,security FROM remote WHERE id=:id AND record=:record');
$query->bindValue(':id', $credential, \PDO::PARAM_INT);
$query->bindValue(':record', $record, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$description = $description !== null ? $description : $record['description'];
$type = $type !== null ? $type : $record['type'];
if ($type === 'key') {
if (openssl_pkey_get_public($key) === false) {
throw new \Exceptions\InvalidKeyException();
}
$secret = $key;
} elseif ($type === 'password') {
$secret = password_hash($password, PASSWORD_DEFAULT);
} elseif ($type === null) {
$secret = null;
} else {
throw new \Exceptions\SemanticException();
}
$query = $this->db->prepare('UPDATE remote SET description=:description,type=:type,security=:security WHERE id=:credential');
$query->bindValue(':description', $description);
$query->bindValue(':type', $type);
$query->bindValue(':security', $secret);
$query->bindValue(':credential', $credential);
$query->execute();
$this->db->commit();
return $record;
}
}

View file

@ -0,0 +1,293 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying domains.
*/
class Domains
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of domains according to filter criteria
*
* @param $pi PageInfo object, which is also updated with total page number
* @param $userId Id of the user for which the table should be retrieved
* @param $query Search query to search in the domain name, null for no filter
* @param $sorting Sort string in format 'field-asc,field2-desc', null for default
* @param $type Type to filter for, null for no filter
*
* @return array Array with matching domains
*/
public function getDomains(\Utils\PagingInfo &$pi, int $userId, ? string $query, ? string $sorting, ? string $type) : array
{
$this->db->beginTransaction();
$ac = new \Operations\AccessControl($this->c);
$userIsAdmin = $ac->isAdmin($userId);
$queryStr = $query === null ? '%' : '%' . $query . '%';
//Count elements
if ($pi->pageSize === null) {
$pi->totalPages = 1;
} else {
$query = $this->db->prepare('
SELECT COUNT(*) AS total
FROM domains D
LEFT OUTER JOIN permissions P ON D.id = P.domain_id
WHERE (P.user_id=:userId OR :userIsAdmin) AND
(D.name LIKE :nameQuery) AND
(D.type = :domainType OR :noTypeFilter)
');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':userIsAdmin', intval($userIsAdmin), \PDO::PARAM_INT);
$query->bindValue(':nameQuery', $queryStr, \PDO::PARAM_STR);
$query->bindValue(':domainType', (string)$type, \PDO::PARAM_STR);
$query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
$pi->totalPages = ceil($record['total'] / $pi->pageSize);
}
//Query and return result
$ordStr = \Services\Database::makeSortingString($sorting, [
'id' => 'D.id',
'name' => 'D.name',
'type' => 'D.type',
'records' => 'records'
]);
$pageStr = \Services\Database::makePagingString($pi);
$query = $this->db->prepare('
SELECT D.id,D.name,D.type,D.master,count(R.domain_id) AS records
FROM domains D
LEFT OUTER JOIN records R ON D.id = R.domain_id AND R.type <> \'SOA\'
LEFT OUTER JOIN permissions P ON D.id = P.domain_id
WHERE (P.user_id=:userId OR :userIsAdmin) AND
(R.type <> \'SOA\' OR R.type IS NULL)
GROUP BY D.id
HAVING
(D.name LIKE :nameQuery) AND
(D.type=:domainType OR :noTypeFilter)'
. $ordStr . $pageStr);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':userIsAdmin', intval($userIsAdmin), \PDO::PARAM_INT);
$query->bindValue(':nameQuery', $queryStr, \PDO::PARAM_STR);
$query->bindValue(':domainType', (string)$type, \PDO::PARAM_STR);
$query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
$query->execute();
$data = $query->fetchAll();
$this->db->commit();
return array_map(function ($item) {
if ($item['type'] != 'SLAVE') {
unset($item['master']);
}
$item['id'] = intval($item['id']);
$item['records'] = intval($item['records']);
return $item;
}, $data);
}
/**
* Add new domain
*
* @param $name Name of the new zone
* @param $type Type of the new zone
* @param $master Master for slave zones, otherwise null
*
* @return array New domain entry
*
* @throws AlreadyExistenException it the domain exists already
*/
public function addDomain(string $name, string $type, ? string $master) : array
{
if (!in_array($type, ['MASTER', 'SLAVE', 'NATIVE'])) {
throw new \Exceptions\SemanticException();
}
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM domains WHERE name=:name');
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
if ($record !== false) { // Domain already exists
$this->db->rollBack();
throw new \Exceptions\AlreadyExistentException();
}
if ($type === 'SLAVE') {
$query = $this->db->prepare('INSERT INTO domains (name, type, master) VALUES(:name, :type, :master)');
$query->bindValue(':master', $master, \PDO::PARAM_STR);
} else {
$query = $this->db->prepare('INSERT INTO domains (name, type) VALUES(:name, :type)');
}
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->bindValue(':type', $type, \PDO::PARAM_STR);
$query->execute();
$query = $this->db->prepare('SELECT id,name,type,master FROM domains WHERE name=:name');
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
$record['id'] = intval($record['id']);
if ($type !== 'SLAVE') {
unset($record['master']);
}
$this->db->commit();
return $record;
}
/**
* Delete domain
*
* @param $id Id of the domain to delete
*
* @return void
*
* @throws NotFoundException if domain does not exist
*/
public function deleteDomain(int $id) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM domains WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) { //Domain does not exist
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('
DELETE E FROM remote E
LEFT OUTER JOIN records R ON R.id=E.record
WHERE R.domain_id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$query = $this->db->prepare('DELETE FROM records WHERE domain_id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$query = $this->db->prepare('DELETE FROM domains WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$this->db->commit();
}
/**
* Get domain
*
* @param $id Id of the domain to get
*
* @return array Domain data
*
* @throws NotFoundException if domain does not exist
*/
public function getDomain(int $id) : array
{
$query = $this->db->prepare('
SELECT D.id,D.name,D.type,D.master,COUNT(R.domain_id) AS records FROM domains D
LEFT OUTER JOIN records R ON D.id = R.domain_id AND R.type <> \'SOA\'
WHERE D.id=:id AND (R.type <> \'SOA\' OR R.type IS NULL)
GROUP BY D.id,D.name,D.type,D.master
');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
$record['id'] = intval($record['id']);
$record['records'] = intval($record['records']);
if ($record['type'] !== 'SLAVE') {
unset($record['master']);
}
return $record;
}
/**
* Get type of given domain
*
* @param int Domain id
*
* @return string Domain type
*
* @throws NotFoundException if domain does not exist
*/
public function getDomainType(int $id) : string
{
$query = $this->db->prepare('SELECT type FROM domains WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
return $record['type'];
}
/**
* Update master for slave zone
*
* @param int Domain id
* @param string New master
*
* @return void
*
* @throws NotFoundException if domain does not exist
* @throws SemanticException if domain is no slave zone
*/
public function updateSlave(int $id, string $master)
{
if ($this->getDomainType($id) !== 'SLAVE') {
throw new \Exceptions\SemanticException();
}
$query = $this->db->prepare('UPDATE domains SET master=:master WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->bindValue(':master', $master, \PDO::PARAM_STR);
$query->execute();
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying permissions.
*/
class Permissions
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of permissions
*
* @param $pi PageInfo object, which is also updated with total page number
* @param $userId Id of the user for which the permissions should be retrieved
*
* @return array Array with matching permissions
*/
public function getPermissions(\Utils\PagingInfo &$pi, int $userId) : array
{
$this->db->beginTransaction();
//Count elements
if ($pi->pageSize === null) {
$pi->totalPages = 1;
} else {
$query = $this->db->prepare('SELECT COUNT(*) AS total FROM permissions WHERE user_id=:userId');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
$pi->totalPages = ceil($record['total'] / $pi->pageSize);
}
$pageStr = \Services\Database::makePagingString($pi);
$query = $this->db->prepare('
SELECT P.domain_id as domainId,D.name as domainName FROM permissions P
LEFT OUTER JOIN domains D ON D.id=P.domain_id
WHERE P.user_id=:userId'
. $pageStr);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
$data = $query->fetchAll();
$this->db->commit();
return $data;
}
/**
* Add a new permission
*
* @param $userId User id
* @param $domainId Domain for which access should be granted
*
* @return void
*
* @throws NotFoundException If domain or user was not found
*/
public function addPermission(int $userId, int $domainId) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM users WHERE id=:userId');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('SELECT id FROM domains WHERE id=:domainId');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('SELECT * FROM permissions WHERE domain_id=:domainId AND user_id=:userId');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
$query = $this->db->prepare('INSERT INTO permissions (domain_id,user_id) VALUES (:domainId, :userId)');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
}
$this->db->commit();
}
/**
* Delete a permission
*
* @param $userId User id
* @param $domainId Domain for which access should be revoked
*
* @return void
*
* @throws NotFoundException if the entry was not found
*/
public function deletePermission(int $userId, int $domainId) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT * FROM permissions WHERE domain_id=:domainId AND user_id=:userId');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('DELETE FROM permissions WHERE domain_id=:domainId AND user_id=:userId');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->execute();
$this->db->commit();
}
}

View file

@ -0,0 +1,328 @@
<?php
namespace Operations;
use function Monolog\Handler\error_log;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying domains.
*/
class Records
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of records according to filter criteria
*
* @param $pi PageInfo object, which is also updated with total page number
* @param $userId Id of the user for which the table should be retrieved
* @param $domain Comma separated list of domain ids
* @param $queryName Search query to search in the record name, null for no filter
* @param $type Comma separated list of types
* @param $queryContent Search query to search in the record content, null for no filter
* @param $sort Sort string in format 'field-asc,field2-desc', null for default
*
* @return array Array with matching records
*/
public function getRecords(
\Utils\PagingInfo &$pi,
int $userId,
? string $domain,
? string $queryName,
? string $type,
? string $queryContent,
? string $sort
) : array {
$this->db->beginTransaction();
$ac = new \Operations\AccessControl($this->c);
$userIsAdmin = $ac->isAdmin($userId);
$queryName = $queryName === null ? '%' : '%' . $queryName . '%';
$queryContent = $queryContent === null ? '%' : '%' . $queryContent . '%';
$setDomains = \Services\Database::makeSetString($this->db, $domain);
$setTypes = \Services\Database::makeSetString($this->db, $type);
//Count elements
if ($pi->pageSize === null) {
$pi->totalPages = 1;
} else {
$query = $this->db->prepare('
SELECT COUNT(*) AS total FROM records R
LEFT OUTER JOIN domains D ON R.domain_id = D.id
LEFT OUTER JOIN permissions P ON P.domain_id = R.domain_id
WHERE (P.user_id=:userId OR :userIsAdmin) AND
(R.domain_id IN ' . $setDomains . ' OR :noDomainFilter) AND
(R.name LIKE :queryName) AND
(R.type IN ' . $setTypes . ' OR :noTypeFilter) AND
(R.content LIKE :queryContent) AND
R.type <> \'SOA\'
');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':userIsAdmin', intval($userIsAdmin), \PDO::PARAM_INT);
$query->bindValue(':queryName', $queryName, \PDO::PARAM_STR);
$query->bindValue(':queryContent', $queryContent, \PDO::PARAM_STR);
$query->bindValue(':noDomainFilter', intval($domain === null), \PDO::PARAM_INT);
$query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
$pi->totalPages = ceil($record['total'] / $pi->pageSize);
}
//Query and return result
$ordStr = \Services\Database::makeSortingString($sort, [
'id' => 'R.id',
'name' => 'R.name',
'type' => 'R.type',
'content' => 'R.content',
'priority' => 'R.prio',
'ttl' => 'R.ttl'
]);
$pageStr = \Services\Database::makePagingString($pi);
$query = $this->db->prepare('
SELECT R.id,R.name,R.type,R.content,R.prio as priority,R.ttl,R.domain_id as domain FROM records R
LEFT OUTER JOIN domains D ON R.domain_id = D.id
LEFT OUTER JOIN permissions P ON P.domain_id = R.domain_id
WHERE (P.user_id=:userId OR :userIsAdmin) AND
(R.domain_id IN ' . $setDomains . ' OR :noDomainFilter) AND
(R.name LIKE :queryName) AND
(R.type IN ' . $setTypes . ' OR :noTypeFilter) AND
(R.content LIKE :queryContent) AND
R.type <> \'SOA\'
GROUP BY R.id' . $ordStr . $pageStr);
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':userIsAdmin', intval($userIsAdmin), \PDO::PARAM_INT);
$query->bindValue(':queryName', $queryName, \PDO::PARAM_STR);
$query->bindValue(':queryContent', $queryContent, \PDO::PARAM_STR);
$query->bindValue(':noDomainFilter', intval($domain === null), \PDO::PARAM_INT);
$query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
$query->execute();
$data = $query->fetchAll();
$this->db->commit();
return array_map(function ($item) {
$item['id'] = intval($item['id']);
$item['priority'] = intval($item['priority']);
$item['ttl'] = intval($item['ttl']);
$item['domain'] = intval($item['domain']);
return $item;
}, $data);
}
/**
* Add new record
*
* @param $name Name of the new record
* @param $type Type of the new record
* @param $content Content of the new record
* @param $priority Priority of the new record
* @param $ttl TTL of the new record
* @param $domain Domain id of the domain to add the record
*
* @return array New record entry
*
* @throws NotFoundException if the domain does not exist
* @throws SemanticException if the record type is invalid
*/
public function addRecord(string $name, string $type, string $content, int $priority, int $ttl, int $domain) : array
{
if (!in_array($type, $this->c['config']['records']['allowedTypes'])) {
throw new \Exceptions\SemanticException();
}
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM domains WHERE id=:id AND type IN (\'MASTER\',\'NATIVE\')');
$query->bindValue(':id', $domain, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) { // Domain does not exist
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('INSERT INTO records (domain_id, name, type, content, ttl, prio, change_date)
VALUES (:domainId, :name, :type, :content, :ttl, :prio, :changeDate)');
$query->bindValue(':domainId', $domain, \PDO::PARAM_INT);
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->bindValue(':type', $type, \PDO::PARAM_STR);
$query->bindValue(':content', $content, \PDO::PARAM_STR);
$query->bindValue(':ttl', $ttl, \PDO::PARAM_INT);
$query->bindValue(':prio', $priority, \PDO::PARAM_INT);
$query->bindValue(':changeDate', time(), \PDO::PARAM_INT);
$query->execute();
$query = $this->db->prepare('SELECT id,name,type,content,prio AS priority,ttl,domain_id AS domain FROM records
ORDER BY id DESC LIMIT 1');
$query->execute();
$record = $query->fetch();
$record['id'] = intval($record['id']);
$record['priority'] = intval($record['priority']);
$record['ttl'] = intval($record['ttl']);
$record['domain'] = intval($record['domain']);
$soa = new \Operations\Soa($this->c);
$soa->updateSerial($domain);
$this->db->commit();
return $record;
}
/**
* Delete record
*
* @param $id Id of the record to delete
*
* @return void
*
* @throws NotFoundException if record does not exist
*/
public function deleteRecord(int $id) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,domain_id FROM records WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) { //Domain does not exist
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$domainId = intval($record['domain_id']);
$query = $this->db->prepare('DELETE FROM remote WHERE record=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$query = $this->db->prepare('DELETE FROM records WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$soa = new \Operations\Soa($this->c);
$soa->updateSerial($domainId);
$this->db->commit();
}
/**
* Get record
*
* @param $recordId Name of the record
*
* @return array Record entry
*
* @throws NotFoundException if the record does not exist
*/
public function getRecord(int $recordId) : array
{
$query = $this->db->prepare('SELECT id,name,type,content,prio AS priority,ttl,domain_id AS domain FROM records
WHERE id=:recordId');
$query->bindValue(':recordId', $recordId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
$record['id'] = intval($record['id']);
$record['priority'] = intval($record['priority']);
$record['ttl'] = intval($record['ttl']);
$record['domain'] = intval($record['domain']);
return $record;
}
/** Update Record
*
* If params are null do not change
*
* @param $recordId Record to update
* @param $name New name
* @param $type New type
* @param $content New content
* @param $priority New priority
* @param $ttl New ttl
*
* @return void
*
* @throws NotFoundException The given record does not exist
* @throws SemanticException The given record type is invalid
*/
public function updateRecord(int $recordId, ? string $name, ? string $type, ? string $content, ? int $priority, ? int $ttl)
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,domain_id,name,type,content,prio,ttl FROM records WHERE id=:recordId');
$query->bindValue(':recordId', $recordId);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
if ($type !== null && !in_array($type, $this->c['config']['records']['allowedTypes'])) {
throw new \Exceptions\SemanticException();
}
$domainId = intval($record['domain_id']);
$name = $name === null ? $record['name'] : $name;
$type = $type === null ? $record['type'] : $type;
$content = $content === null ? $record['content'] : $content;
$priority = $priority === null ? intval($record['prio']) : $priority;
$ttl = $ttl === null ? intval($record['ttl']) : $ttl;
$query = $this->db->prepare('UPDATE records SET name=:name,type=:type,content=:content,prio=:priority,ttl=:ttl WHERE id=:recordId');
$query->bindValue('recordId', $recordId);
$query->bindValue(':name', $name);
$query->bindValue(':type', $type);
$query->bindValue(':content', $content);
$query->bindValue(':priority', $priority);
$query->bindValue(':ttl', $ttl);
$query->execute();
$soa = new \Operations\Soa($this->c);
$soa->updateSerial($domainId);
$this->db->commit();
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides functions for the remote api.
*/
class Remote
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Update given record with password
*
* @param $record Name of the new record
* @param $content Type of the new record
* @param $password Content of the new record
*
* @throws NotFoundException if the record does not exist
* @throws ForbiddenException if the password is not valid for the record
*/
public function updatePassword(int $record, string $content, string $password) : void
{
$query = $this->db->prepare('SELECT id FROM records WHERE id=:record');
$query->bindValue(':record', $record, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('SELECT security FROM remote WHERE record=:record AND type=\'password\'');
$query->bindValue(':record', $record, \PDO::PARAM_INT);
$query->execute();
$validPwFound = false;
while ($row = $query->fetch()) {
if (password_verify($password, $row['security'])) {
$validPwFound = true;
break;
}
}
if (!$validPwFound) {
throw new \Exceptions\ForbiddenException();
}
$records = new \Operations\Records($this->c);
$records->updateRecord($record, null, null, $content, null, null);
}
/**
* Update given record with password
*
* @param $record Name of the new record
* @param $content Type of the new record
* @param $time Timestamp of the signature
* @param $signature Signature
*
* @throws NotFoundException if the record does not exist
* @throws ForbiddenException if the signature is not valid for the record
*/
public function updateKey(int $record, string $content, int $time, string $signature) : void
{
$timestampWindow = $this->c['config']['remote']['timestampWindow'];
$query = $this->db->prepare('SELECT id FROM records WHERE id=:record');
$query->bindValue(':record', $record, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) {
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('SELECT security FROM remote WHERE record=:record AND type=\'key\'');
$query->bindValue(':record', $record, \PDO::PARAM_INT);
$query->execute();
if (abs($time - time()) > $timestampWindow) {
throw new \Exceptions\ForbiddenException();
}
$validKeyFound = false;
$verifyString = $record . $content . $time;
while ($row = $query->fetch()) {
if (openssl_verify($verifyString, base64_decode($signature), $row['security'], OPENSSL_ALGO_SHA512)) {
$validKeyFound = true;
break;
}
}
if (!$validKeyFound) {
throw new \Exceptions\ForbiddenException();
}
$records = new \Operations\Records($this->c);
$records->updateRecord($record, null, null, $content, null, null);
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This is a proxy class which load the configured plugin as
* backend and proxies queries to it.
*/
class Sessionstorage
{
/** @var \Monolog\Logger */
private $logger;
/** @var InterfaceSessionstorage */
private $backend;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$config = $c['config']['sessionstorage'];
$plugin = $config['plugin'];
$pluginConfig = $config['config'];
$pluginClass = '\\Plugins\\Sessionstorage\\' . $plugin;
//Check if plugin is available
if (!class_exists($pluginClass)) {
$this->logger->critical('The configured session storage plugin does not exist', ['plugin' => $plugin]);
exit();
}
//Try to create class with given name
$this->backend = new $pluginClass($this->logger, $pluginConfig);
if (!$this->backend instanceof \Plugins\Sessionstorage\InterfaceSessionstorage) {
$this->logger->critical('The configured plugin does not implement InterfaceSessionstorage', ['pluginname' => $plugin]);
exit();
}
$this->logger->debug("Session storage plugin was loaded", ['plugin' => $plugin]);
}
public function set(string $key, string $value, int $ttl) : void
{
$this->backend->set($key, $value, $ttl);
}
public function exists(string $key) : bool
{
return $this->backend->exists($key);
}
public function get(string $key, int $ttl) : string
{
return $this->backend->get($key, $ttl);
}
public function delete(string $key) : void
{
$this->backend->delete($key);
}
}

View file

@ -0,0 +1,220 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying soa records.
*/
class Soa
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of domains according to filter criteria
*
* @param $domainId Domain to update soa
* @param $mail Mail of zone master
* @param $primary The primary nameserver
* @param $refresh The refresh interval
* @param $retry The retry interval
* @param $expire The expire timeframe
* @param $ttl The zone ttl
*
* @return void
*
* @throws NotFoundException If the given domain does not exist
*/
public function setSoa(int $domainId, string $mail, string $primary, int $refresh, int $retry, int $expire, int $ttl)
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,name,type FROM domains WHERE id=:id');
$query->bindValue(':id', $domainId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
} elseif ($record['type'] === 'SLAVE') {
$this->db->rollBack();
throw new \Exceptions\SemanticException();
} else {
$domainName = $record['name'];
}
//Generate soa content string without serial
$soaArray = [
$primary,
$this->fromEmail($mail),
'serial',
$refresh,
$retry,
$expire,
$ttl
];
$query = $this->db->prepare('SELECT content FROM records WHERE domain_id=:id AND type=\'SOA\'');
$query->bindValue(':id', $domainId, \PDO::PARAM_INT);
$query->execute();
$content = $query->fetch();
if ($content === false) { //No soa exists yet
$soaArray[2] = strval($this->calculateSerial(0));
$soaString = implode(' ', $soaArray);
$changeDate = strval(time());
$query = $this->db->prepare('
INSERT INTO records (domain_id, name, type, content, ttl, change_date)
VALUES (:domainId, :name, \'SOA\', :content, :ttl, :changeDate)
');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':name', $domainName, \PDO::PARAM_STR);
$query->bindValue(':content', $soaString, \PDO::PARAM_STR);
$query->bindValue(':ttl', $ttl, \PDO::PARAM_STR);
$query->bindValue(':changeDate', $changeDate, \PDO::PARAM_INT);
$query->execute();
} else {
$oldSerial = intval(explode(' ', $content['content'])[2]);
$soaArray[2] = strval($this->calculateSerial($oldSerial));
$soaString = implode(' ', $soaArray);
$changeDate = strval(time());
$query = $this->db->prepare('UPDATE records SET content=:content, ttl=:ttl,
change_date=:changeDate WHERE domain_id=:domainId AND type=\'SOA\'');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':content', $soaString, \PDO::PARAM_STR);
$query->bindValue(':ttl', $ttl, \PDO::PARAM_STR);
$query->bindValue(':changeDate', $changeDate, \PDO::PARAM_INT);
$query->execute();
}
$this->db->commit();
}
/**
* Get soa record for domain
*
* @param $domainId Domain to get soa from
*
* @return array Soa data as associative array
*/
public function getSoa(int $domainId)
{
$query = $this->db->prepare('SELECT content FROM records WHERE domain_id=:domainId AND type=\'SOA\'');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
$soaArray = explode(' ', $record['content']);
return [
'primary' => $soaArray[0],
'email' => $this->toEmail($soaArray[1]),
'serial' => intval($soaArray[2]),
'refresh' => intval($soaArray[3]),
'retry' => intval($soaArray[4]),
'expire' => intval($soaArray[5]),
'ttl' => intval($soaArray[6])
];
}
/**
* Increases the serial number of the given domain to the next required.
*
* If domain has no present soa record this method does nothing.
*
* @param $domainId Domain to update
*
* @return void
*/
public function updateSerial(int $domainId) : void
{
$query = $this->db->prepare('SELECT content FROM records WHERE domain_id=:id AND type=\'SOA\'');
$query->bindValue(':id', $domainId, \PDO::PARAM_INT);
$query->execute();
$content = $query->fetch();
if ($content === false) {
$this->logger->warning('Trying to update serial of domain without soa set it first', ['domainId' => $domainId]);
return;
}
$soaArray = explode(' ', $content['content']);
$soaArray[2] = strval($this->calculateSerial(intval($soaArray[2])));
$soaString = implode(' ', $soaArray);
$query = $this->db->prepare('UPDATE records SET content=:content WHERE domain_id=:domainId AND type=\'SOA\'');
$query->bindValue(':content', $soaString, \PDO::PARAM_STR);
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->execute();
}
/**
* Calculate new serial from old
*
* @param $oldSerial Old serial number
*
* @return int New serial number
*/
private function calculateSerial(int $oldSerial) : int
{
$time = new \DateTime(null, new \DateTimeZone('UTC'));
$currentTime = intval($time->format('Ymd')) * 100;
return \max($oldSerial + 1, $currentTime);
}
/**
* Convert email to soa mail string
*
* @param $email Email address
*
* @return string Soa email address
*/
private function fromEmail(string $email)
{
$parts = explode('@', $email);
$parts[0] = str_replace('.', '\.', $parts[0]);
$parts[] = '';
return rtrim(implode(".", $parts), ".");
}
/**
* Convert soa mail to mail string
*
* @param $soaMail Soa email address
*
* @return string Email address
*/
private function toEmail(string $soaEmail)
{
$tmp = preg_replace('/([^\\\\])\\./', '\\1@', $soaEmail, 1);
$tmp = preg_replace('/\\\\\\./', ".", $tmp);
$tmp = preg_replace('/\\.$/', "", $tmp);
return $tmp;
}
}

View file

@ -0,0 +1,163 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
use \Exceptions\PluginNotFoundException as PluginNotFoundException;
/**
* This class provides user authentication for the application.
* Its main purpose is to find the apropriate authentication
* plugin. It also ensures that a user entry for that user is
* in the database.
*/
class UserAuth
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Authenticates a user with username/password combination.
*
* @param $username Username
* @param $password Password
*
* @return int -1 if authentication failed, the user id otherwise
*
* @throws \Exceptions\PluginNotFoundExecption if no matching backend can be found
*/
public function authenticate(string $username, string $password) : int
{
if (strpos($username, '/') === false) { // no explicit backend specification
$prefix = 'default';
$name = $username;
} else {
$parts = preg_split('/\//', $username, 2);
$prefix = $parts[0];
$name = $parts[1];
}
$this->logger->debug('Trying to authenticate with info', ['prefix' => $prefix, 'name' => $name]);
try {
$backend = '';
if ($this->authenticateBackend($prefix, $name, $password, $backend)) {
return $this->localUser($backend, $name, $password);
} else {
return -1;
}
} catch (\Exceptions\PluginNotFoundException $e) {
throw $e;
}
}
/**
* This function searches for an apropriate backend and calls it
* to authenticate the user.
*
* @param $backend The name of the backend to use
* @param $username The username to use
* @param $password The password to use
* @param $backendId Output to return the backend id used
*
* @return bool true if authentication successfull false otherwise
*
* @throws \Exceptions\PluginNotFoundExecption if no matching backend can be found
*/
private function authenticateBackend(string $backend, string $username, string $password, string &$backendId) : bool
{
$config = $this->c['config']['authentication'];
$configForPrefix = array_filter($config, function ($v, $k) use ($backend) {
return $backend === $v['prefix'];
}, ARRAY_FILTER_USE_BOTH);
if (count($configForPrefix) === 0) { // Check if backend is configured for prefix
$this->logger->warning('No authentication backend configured for prefix', ['prefix' => $backend]);
throw new PluginNotFoundException('No authentication backend configured for this user.');
} elseif (count($configForPrefix) > 1) {
$this->logger->error('Two authentication backends configured for prefix.', ['prefix' => $backend]);
}
$backendId = array_keys($configForPrefix)[0];
$plugin = $config[$backendId]['plugin'];
$pluginClass = '\\Plugins\\UserAuth\\' . $plugin;
$pluginConfig = $config[$backendId]['config'];
if (!class_exists($pluginClass)) { // Check if given backend class exists
$this->logger->error('The configured UserAuth plugin does not exist', ['prefix' => $backend, 'plugin' => $plugin]);
throw new PluginNotFoundException('The authentication request can not be processed.');
}
//Try to create class with given name
$backendObj = new $pluginClass($this->logger, $this->db, $pluginConfig);
if (!$backendObj instanceof \Plugins\UserAuth\InterfaceUserAuth) { // Check if class implements interface
$this->logger->error('The configured plugin does not implement InterfaceUserAuth', ['plugin' => $plugin, 'prefix' => $backend]);
throw new PluginNotFoundException('The authentication request can not be processed.');
}
$this->logger->debug("UserAuth plugin was loaded", ['plugin' => $plugin, 'prefix' => $backend, 'backend' => $backendId]);
return $backendObj->authenticate($username, $password);
}
/**
* Ensures the user from the given backend has a entry in the local database,
* then returns the user id.
*
* @param $backend The name of the backend to use
* @param $username The username to use
* @param $password The password to use
*
* @return int The local user id
*/
private function localUser(string $backend, string $username, string $password) : int
{
$config = $this->c['config']['authentication'];
$backendId = $config[$backend]['plugin'];
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM users WHERE name=:name AND backend=:backend');
$query->bindValue(':name', $username, \PDO::PARAM_STR);
$query->bindValue(':backend', $backendId, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$insert = $this->db->prepare('INSERT INTO users (name,backend,type) VALUES (:name, :backend, \'user\')');
$insert->bindValue(':name', $username, \PDO::PARAM_STR);
$insert->bindValue(':backend', $backendId, \PDO::PARAM_STR);
$insert->execute();
$query->execute();
$record = $query->fetch();
$this->logger->info('Non existing user created', ['username' => $username, 'backendId' => $backendId, 'newId' => $record['id']]);
} else {
$this->logger->debug('User was found in database', ['username' => $username, 'backendId' => $backendId, 'id' => $record['id']]);
}
$this->db->commit();
return $record['id'];
}
}

View file

@ -0,0 +1,309 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying users.
*/
class Users
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of users according to filter criteria
*
* @param $pi PageInfo object, which is also updated with total page number
* @param $nameQuery Search query, may be null
* @param $type Type of the user, comma separated, null for no filter
* @param $sorting Sort string in format 'field-asc,field2-desc', null for default
*
* @return array Array with matching users
*/
public function getUsers(\Utils\PagingInfo &$pi, ? string $nameQuery, ? string $type, ? string $sorting) : array
{
$config = $this->c['config']['authentication'];
$this->db->beginTransaction();
$nameQuery = $nameQuery !== null ? '%' . $nameQuery . '%' : '%';
//Count elements
if ($pi->pageSize === null) {
$pi->totalPages = 1;
} else {
$query = $this->db->prepare('
SELECT COUNT(*) AS total
FROM users U
WHERE (U.name LIKE :nameQuery) AND
(U.type IN ' . \Services\Database::makeSetString($this->db, $type) . ' OR :noTypeFilter)
');
$query->bindValue(':nameQuery', $nameQuery, \PDO::PARAM_STR);
$query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
$pi->totalPages = ceil($record['total'] / $pi->pageSize);
}
//Query and return result
$ordStr = \Services\Database::makeSortingString($sorting, [
'id' => 'U.id',
'name' => 'U.name',
'type' => 'U.type'
]);
$pageStr = \Services\Database::makePagingString($pi);
$query = $this->db->prepare('
SELECT id, name, type, backend
FROM users U
WHERE (U.name LIKE :nameQuery) AND
(U.type IN ' . \Services\Database::makeSetString($this->db, $type) . ' OR :noTypeFilter)'
. $ordStr . $pageStr);
$query->bindValue(':nameQuery', $nameQuery, \PDO::PARAM_STR);
$query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
$query->execute();
$data = $query->fetchAll();
$this->db->commit();
$dataTransformed = array_map(
function ($item) use ($config) {
if (!array_key_exists($item['backend'], $config)) {
return null;
}
if (!array_key_exists('prefix', $config[$item['backend']])) {
return null;
}
$prefix = $config[$item['backend']]['prefix'];
if ($prefix === 'default') {
$name = $item['name'];
} else {
$name = $prefix . '/' . $item['name'];
}
return [
'id' => intval($item['id']),
'name' => $name,
'type' => $item['type'],
'native' => $item['backend'] === 'native'
];
},
$data
);
return array_filter($dataTransformed, function ($v) {
return $v !== null;
});
}
/**
* Add new user
*
* @param $name Name of the new zone
* @param $type Type of the new zone
* @param $password Password for the new user
*
* @return array New user entry
*
* @throws AlreadyExistenException it the user exists already
*/
public function addUser(string $name, string $type, string $password) : array
{
if (!in_array($type, ['admin', 'user'])) {
throw new \Exceptions\SemanticException();
}
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM users WHERE name=:name AND backend=\'native\'');
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
if ($record !== false) { // Domain already exists
$this->db->rollBack();
throw new \Exceptions\AlreadyExistentException();
}
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$query = $this->db->prepare('INSERT INTO users (name, backend, type, password) VALUES(:name, \'native\', :type, :password)');
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->bindValue(':type', $type, \PDO::PARAM_STR);
$query->bindValue(':password', $passwordHash, \PDO::PARAM_STR);
$query->execute();
$query = $this->db->prepare('SELECT id,name,type FROM users WHERE name=:name AND backend=\'native\'');
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
$record['id'] = intval($record['id']);
$this->db->commit();
return $record;
}
/**
* Delete user
*
* @param $id Id of the user to delete
*
* @return void
*
* @throws NotFoundException if user does not exist
*/
public function deleteDomain(int $id) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM users WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
if ($query->fetch() === false) { //User does not exist
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('DELETE FROM permissions WHERE user_id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$query = $this->db->prepare('DELETE FROM users WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$this->db->commit();
}
/**
* Get user
*
* @param $id Id of the user to get
*
* @return array User data
*
* @throws NotFoundException if user does not exist
*/
public function getUser(int $id) : array
{
$config = $this->c['config']['authentication'];
$query = $this->db->prepare('SELECT id,name,type,backend FROM users WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
if (!array_key_exists($record['backend'], $config)) {
throw new \Exceptions\NotFoundException();
}
if (!array_key_exists('prefix', $config[$record['backend']])) {
throw new \Exceptions\NotFoundException();
}
$prefix = $config[$record['backend']]['prefix'];
if ($prefix === 'default') {
$name = $record['name'];
} else {
$name = $prefix . '/' . $record['name'];
}
return [
'id' => intval($record['id']),
'name' => $name,
'type' => $record['type'],
'native' => $record['backend'] === 'native'
];
}
/** Update user
*
* If params are null do not change. If user is not native, name and password are ignored.
*
* @param $userId User to update
* @param $name New name
* @param $type New type
* @param $password New password
*
* @return void
*
* @throws NotFoundException The given record does not exist
* @throws AlreadyExistentException The given record name does already exist
*/
public function updateUser(int $userId, ? string $name, ? string $type, ? string $password)
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,name,type,backend,password FROM users WHERE id=:userId');
$query->bindValue(':userId', $userId);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
if ($record['backend'] !== 'native') {
$name = null;
$password = null;
}
if ($record['backend'] === 'native' && $name !== null) {
//Check if user with new name already exists
$query = $this->db->prepare('SELECT id FROM users WHERE name=:name AND backend=\'native\'');
$query->bindValue(':name', $name);
$query->execute();
$recordTest = $query->fetch();
if ($recordTest !== false && intval($recordTest['id']) !== $userId) {
throw new \Exceptions\AlreadyExistentException();
}
}
$name = $name === null ? $record['name'] : $name;
$type = $type === null ? $record['type'] : $type;
$password = $password === null ? $record['password'] : password_hash($password, PASSWORD_DEFAULT);
$query = $this->db->prepare('UPDATE users SET name=:name,type=:type,password=:password WHERE id=:userId');
$query->bindValue(':userId', $userId);
$query->bindValue(':name', $name);
$query->bindValue(':type', $type);
$query->bindValue(':password', $password);
$query->execute();
$this->db->commit();
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Plugins\Sessionstorage;
require '../vendor/autoload.php';
/**
* This interface provides the neccessary functions for a session storage backend
*/
interface InterfaceSessionstorage
{
/**
* Construct the object
*
* @param $logger Monolog logger instance for error handling
* @param $config The configuration for the Plugin if any was provided
*/
public function __construct(\Monolog\Logger $logger, array $config = null);
/**
* Save new entry.
*
* @param $key The key for the entry
* @param $value The value for the entry
* @param $ttl The time (in s) for which this item should be available
*/
public function set(string $key, string $value, int $ttl) : void;
/**
* Queries the existence of some entry.
*
* @param $key The key to query
*/
public function exists(string $key) : bool;
/**
* Get the value for a given key. This should also reset the ttl to the given value.
*
* @param $key The key for the entry to get
* @param $ttl The new ttl for the entry
*/
public function get(string $key, int $ttl) : string;
/**
* Delete the value for a given key.
*
* @param $key The key to delete
*/
public function delete(string $key) : void;
}

View file

@ -0,0 +1,90 @@
<?php
namespace Plugins\Sessionstorage;
require '../vendor/autoload.php';
/**
* Implements a session storage plugin for using PHPs APCu.
*/
class apcu implements InterfaceSessionstorage
{
/** @var \Monolog\Logger */
private $logger;
/**
* Construct the object
*
* @param $logger Monolog logger instance for error handling
* @param $config The configuration for the Plugin if any was provided
*/
public function __construct(\Monolog\Logger $logger, array $config = null)
{
$this->logger = $logger;
if (!function_exists('apcu_store')) {
$this->logger->critical('PHP APCu extension is not available but configured as session storage backend exiting now');
exit();
}
}
/**
* Save new entry.
*
* @param $key The key for the entry
* @param $value The value for the entry
* @param $ttl The time (in s) for which this item should be available
*/
public function set(string $key, string $value, int $ttl) : void
{
$this->logger->debug('Storing data to APCu', ['key' => $key, 'value' => $value, 'ttl' => $ttl]);
apcu_store($key, $value, $ttl);
}
/**
* Queries the existence of some entry.
*
* @param $key The key to query
*/
public function exists(string $key) : bool
{
$this->logger->debug('Checking for APCu key existence', ['key' => $key]);
return apcu_exists($key);
}
/**
* Get the value for a given key. This should also reset the ttl to the given value.
*
* @param $key The key for the entry to get
* @param $ttl The new ttl for the entry
*/
public function get(string $key, int $ttl) : string
{
$this->logger->debug('Getting data from APCu', ['key' => $key, 'ttl' => $ttl]);
$value = apcu_fetch($key);
if ($value == false) {
$this->logger->error('Non existing key was queried from APCu', ['key' => $key]);
throw new \InvalidArgumentException('The requested key was not in the database!');
}
apcu_store($key, $value, $ttl);
return $value;
}
/**
* Delete the value for a given key.
*
* @param $key The key to delete
*/
public function delete(string $key) : void
{
$this->logger->debug('Deleting key from APCu', ['key' => $key]);
apcu_delete($key);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Plugins\UserAuth;
require '../vendor/autoload.php';
/**
* This interface provides the neccessary functions for
* a user authentication backend.
*/
interface InterfaceUserAuth
{
/**
* Construct the object
*
* @param $logger Monolog logger instance for error handling
* @param $db Database connection
* @param $config The configuration for the Plugin if any was provided
*/
public function __construct(\Monolog\Logger $logger, \PDO $db, array $config = null);
/**
* Authenticate user.
*
* @param $username The username for authentication
* @param $password The password for authentication
*
* @return true if valid false otherwise
*/
public function authenticate(string $username, string $password) : bool;
}

View file

@ -0,0 +1,54 @@
<?php
namespace Plugins\UserAuth;
require '../vendor/autoload.php';
/**
* This provides a simple user auth mechanism where users can be
* stored in the config file. The config property therefore should
* be a array mapping usernames to results of password_hash()
*/
class Config implements InterfaceUserAuth
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var array */
private $userList;
/**
* Construct the object
*
* @param $logger Monolog logger instance for error handling
* @param $db Database connection
* @param $config The configuration for the Plugin if any was provided
*/
public function __construct(\Monolog\Logger $logger, \PDO $db, array $config = null)
{
$this->logger = $logger;
$this->db = $db;
$this->userList = $config ? $config : [];
}
/**
* Authenticate user.
*
* @param $username The username for authentication
* @param $password The password for authentication
*
* @return true if valid false otherwise
*/
public function authenticate(string $username, string $password) : bool
{
if (!array_key_exists($username, $this->userList)) {
return false;
}
return password_verify($password, $this->userList[$username]);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Plugins\UserAuth;
require '../vendor/autoload.php';
/**
* This provides the native authentication done in the
* PDNSManager database.
*/
class Native implements InterfaceUserAuth
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/**
* Construct the object
*
* @param $logger Monolog logger instance for error handling
* @param $db Database connection
* @param $config The configuration for the Plugin if any was provided
*/
public function __construct(\Monolog\Logger $logger, \PDO $db, array $config = null)
{
$this->logger = $logger;
$this->db = $db;
}
/**
* Authenticate user.
*
* @param $username The username for authentication
* @param $password The password for authentication
*
* @return true if valid false otherwise
*/
public function authenticate(string $username, string $password) : bool
{
$query = $this->db->prepare('SELECT id, password FROM users WHERE name=:name AND backend=\'native\'');
$query->bindValue(':name', $username, \PDO::PARAM_STR);
$query->execute();
$record = $query->fetch();
if ($record === false) {
return false;
}
return password_verify($password, $record['password']);
}
}

View file

@ -0,0 +1,83 @@
<?php
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
// Load config
$config = require('../config/ConfigDefault.php');
// If no config exists load installer
if ($config === false) {
require('setup.php');
exit();
}
// Prepare dependency container
$container = new \Slim\Container($config);
$container['logger'] = new \Services\Logger;
$container['db'] = new \Services\Database;
$container['notFoundHandler'] = new \Controllers\NotFound;
$container['notAllowedHandler'] = new \Controllers\NotAllowed;
// Create application
$app = new \Slim\App($container);
// Configure routing
$app->group('/v1', function () {
$this->post('/sessions', '\Controllers\Sessions:post');
$this->get('/remote/ip', '\Controllers\Remote:ip');
$this->get('/remote/servertime', '\Controllers\Remote:servertime');
$this->get('/remote/updatepw', '\Controllers\Remote:updatePassword');
$this->post('/remote/updatekey', '\Controllers\Remote:updateKey');
$this->get('/update', '\Controllers\Update:get');
$this->post('/update', '\Controllers\Update:post');
$this->group('', function () {
$this->delete('/sessions/{sessionId}', '\Controllers\Sessions:delete');
$this->get('/domains', '\Controllers\Domains:getList');
$this->post('/domains', '\Controllers\Domains:postNew');
$this->delete('/domains/{domainId}', '\Controllers\Domains:delete');
$this->get('/domains/{domainId}', '\Controllers\Domains:getSingle');
$this->put('/domains/{domainId}', '\Controllers\Domains:put');
$this->put('/domains/{domainId}/soa', '\Controllers\Domains:putSoa');
$this->get('/domains/{domainId}/soa', '\Controllers\Domains:getSoa');
$this->get('/records', '\Controllers\Records:getList');
$this->post('/records', '\Controllers\Records:postNew');
$this->delete('/records/{recordId}', '\Controllers\Records:delete');
$this->get('/records/{recordId}', '\Controllers\Records:getSingle');
$this->put('/records/{recordId}', '\Controllers\Records:put');
$this->get('/records/{recordId}/credentials', '\Controllers\Credentials:getList');
$this->post('/records/{recordId}/credentials', '\Controllers\Credentials:postNew');
$this->delete('/records/{recordId}/credentials/{credentialId}', '\Controllers\Credentials:delete');
$this->get('/records/{recordId}/credentials/{credentialId}', '\Controllers\Credentials:getSingle');
$this->put('/records/{recordId}/credentials/{credentialId}', '\Controllers\Credentials:put');
$this->get('/users', '\Controllers\Users:getList');
$this->post('/users', '\Controllers\Users:postNew');
$this->delete('/users/{user}', '\Controllers\Users:delete');
$this->get('/users/{user}', '\Controllers\Users:getSingle');
$this->put('/users/{user}', '\Controllers\Users:put');
$this->get('/users/{user}/permissions', '\Controllers\Permissions:getList');
$this->post('/users/{user}/permissions', '\Controllers\Permissions:postNew');
$this->delete('/users/{user}/permissions/{domainId}', '\Controllers\Permissions:delete');
})->add('\Middlewares\Authentication');
});
// Add global middlewares
$app->add('\Middlewares\LogRequests');
$app->add('\Middlewares\RejectEmptyBody');
$app->add('\Middlewares\ClientIp');
// Run application
$app->run();

View file

@ -0,0 +1,23 @@
<?php
require '../vendor/autoload.php';
use \Slim\Http\Request as Request;
use \Slim\Http\Response as Response;
if (file_exists('../config/ConfigUser.php')) {
echo "Not accessible!";
http_response_code(403);
exit();
}
// Prepare dependency container
$container = new \Slim\Container();
// Create application
$app = new \Slim\App($container);
// Create route
$app->post('/v1/setup', '\Controllers\Setup:setup');
// Run application
$app->run();

View file

@ -0,0 +1,124 @@
<?php
namespace Services;
require '../vendor/autoload.php';
class Database
{
public function __invoke(\Slim\Container $c)
{
$config = $c['config']['db'];
try {
$pdo = new \PDO(
'mysql:host=' . $config['host'] . ';port=' . $config['port'] . ';dbname=' . $config['dbname'],
$config['user'],
$config['password']
);
} catch (\PDOException $e) {
$c->logger->critical("SQL Connect Error: " . $e->getMessage());
$c->logger->critical("DB Config was", $config);
exit();
}
try {
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
$c->logger->critical("SQL Parameter Error: " . $e->getMessage());
exit();
}
$c->logger->debug("Database setup successfull");
return $pdo;
}
/**
* Makes a SQL LIMIT string from paging information
*
* @param $pi PagingInfo object to use
*
* @return string SQL string to use
*/
public static function makePagingString(\Utils\PagingInfo $pi) : string
{
if ($pi->pageSize === null) {
return '';
}
if ($pi->page === null) {
$pi->page = 1;
}
$offset = ($pi->page - 1) * $pi->pageSize;
return ' LIMIT ' . intval($pi->pageSize) . ' OFFSET ' . intval($offset);
}
/**
* Makes a SQL ORDER BY string from order information.
*
* This is done from a string with format 'field-asc,field2-desc'
* where fields are mapped to columns in param $colMap. This also
* should prevent SQL injections.
*
* @param $sort Sort string
* @param $colMap Map which assigns to each field name a column to use
*
* @return string SQL string to use
*/
public static function makeSortingString(? string $sort, array $colMap) : string
{
if ($sort === null) {
return '';
}
$orderStrings = [];
foreach (explode(',', $sort) as $value) {
$parts = explode('-', $value);
if (array_key_exists($parts[0], $colMap) && count($parts) == 2) { // is valid known field
if ($parts[1] == 'asc') {
$orderStrings[] = $colMap[$parts[0]] . ' ASC';
} elseif ($parts[1] == 'desc') {
$orderStrings[] = $colMap[$parts[0]] . ' DESC';
}
}
}
if (count($orderStrings) == 0) { // none was valid
return '';
}
return ' ORDER BY ' . implode(', ', $orderStrings);
}
/**
* Makes a string which works to use with an IN SQL clause.
*
* Input is a comma separated list, all items are escaped and joint
* to the form ('a','b')
*
* @param $db PDO object used for escaping
* @param $input Comma separated list of items
*
* @return string SQL string to use
*/
public static function makeSetString(\PDO $db, ? string $input) : string
{
if ($input === null || $input === '') {
return '(\'\')';
}
$parts = explode(',', $input);
$partsEscaped = array_map(function ($item) use ($db) {
return $db->quote($item, \PDO::PARAM_STR);
}, $parts);
return '(' . implode(', ', $partsEscaped) . ')';
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Services;
require '../vendor/autoload.php';
class Logger
{
public function __invoke(\Slim\Container $c)
{
$config = $c['config']['logging'];
$logger = new \Monolog\Logger('pdnsmanager');
$loglevel = \Monolog\Logger::toMonologLevel($config['level']);
$path = $config['path'];
if (\strlen($path) > 0) {
$fileHandler = new \Monolog\Handler\StreamHandler($path, $loglevel);
$logger->pushHandler($fileHandler);
} else {
$errorLogHandler = new \Monolog\Handler\ErrorLogHandler(0, $loglevel);
$logger->pushHandler($errorLogHandler);
}
return $logger;
}
}

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS remote (
id int(11) NOT NULL AUTO_INCREMENT,
record int(11) NOT NULL,
description varchar(255) NOT NULL,
type varchar(20) NOT NULL,
security varchar(2000) NOT NULL,
nonce varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
KEY record (record)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `remote`
ADD CONSTRAINT `remote_ibfk_1` FOREIGN KEY (`record`) REFERENCES `records` (`id`);
CREATE TABLE IF NOT EXISTS options (
name varchar(255) NOT NULL,
value varchar(2000) DEFAULT NULL,
PRIMARY KEY (name)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO options(name,value) VALUES ('schema_version', 1);

View file

@ -0,0 +1,22 @@
ALTER TABLE permissions
DROP FOREIGN KEY permissions_ibfk_1;
ALTER TABLE permissions
DROP FOREIGN KEY permissions_ibfk_2;
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_1 FOREIGN KEY (domain) REFERENCES domains (id) ON DELETE CASCADE;
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_2 FOREIGN KEY (user) REFERENCES user (id) ON DELETE CASCADE;
ALTER TABLE remote
DROP FOREIGN KEY remote_ibfk_1;
ALTER TABLE remote
ADD CONSTRAINT remote_ibfk_1 FOREIGN KEY (record) REFERENCES records (id) ON DELETE CASCADE;
ALTER TABLE records
ADD CONSTRAINT records_ibfk_1 FOREIGN KEY (domain_id) REFERENCES domains (id) ON DELETE CASCADE;
UPDATE options SET value=2 WHERE name='schema_version';

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS domainmetadata (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
kind VARCHAR(32),
content TEXT,
PRIMARY KEY (id)
) Engine=InnoDB;
ALTER TABLE records ADD disabled TINYINT(1) DEFAULT 0;
ALTER TABLE records ADD auth TINYINT(1) DEFAULT 1;
UPDATE options SET value=3 WHERE name='schema_version';

View file

@ -0,0 +1,12 @@
ALTER TABLE permissions DROP FOREIGN KEY permissions_ibfk_2;
RENAME TABLE user TO users;
ALTER TABLE permissions CHANGE user userid INT(11);
ALTER TABLE permissions
ADD CONSTRAINT permissions_ibfk_2 FOREIGN KEY (userid) REFERENCES users (id) ON DELETE CASCADE;
ALTER TABLE users ADD CONSTRAINT UNIQUE KEY user_name_index (name);
UPDATE options SET value=4 WHERE name='schema_version';

100
backend/src/sql/Update5.sql Normal file
View file

@ -0,0 +1,100 @@
ALTER DATABASE CHARACTER SET utf8 COLLATE = utf8_general_ci;
ALTER TABLE `domainmetadata`
ADD INDEX domainmetadata_idx (domain_id,kind);
ALTER TABLE `domains`
DROP PRIMARY KEY,
DROP INDEX name_index,
ADD PRIMARY KEY(`id`),
ADD UNIQUE INDEX name_index (name),
CHANGE COLUMN notified_serial notified_serial int(10) unsigned NULL;
ALTER TABLE `permissions`
DROP FOREIGN KEY permissions_ibfk_1,
DROP FOREIGN KEY permissions_ibfk_2,
DROP INDEX domain,
DROP PRIMARY KEY,
CHANGE `userid` `user_id` INT(11) NOT NULL,
CHANGE `domain` `domain_id` INT(11) NOT NULL;
ALTER TABLE `permissions`
ADD CONSTRAINT permissions_ibfk_1 FOREIGN KEY(user_id) REFERENCES `users`(id),
ADD CONSTRAINT permissions_ibfk_2 FOREIGN KEY(domain_id) REFERENCES `domains`(id),
ADD PRIMARY KEY(`domain_id`,`user_id`),
ADD INDEX permissions_ibfk_3 (domain_id),
COLLATE=utf8_general_ci;
ALTER TABLE `records`
DROP FOREIGN KEY records_ibfk_1,
DROP INDEX rec_name_index,
DROP INDEX nametype_index,
DROP PRIMARY KEY,
ADD PRIMARY KEY(`id`),
DROP INDEX domain_id;
ALTER TABLE `records`
ADD CONSTRAINT records_ibfk_1 FOREIGN KEY(domain_id) REFERENCES `domains`(id),
ADD INDEX nametype_index (name,type),
ADD INDEX domain_id (domain_id),
ADD INDEX ordername (ordername),
CHANGE COLUMN content content varchar(64000) NULL,
CHANGE COLUMN type type varchar(10) NULL,
ADD COLUMN ordername varchar(255) NULL AFTER disabled,
CHANGE COLUMN prio prio int(11) NULL,
CHANGE COLUMN auth auth tinyint(1) NULL DEFAULT '1';
ALTER TABLE `remote`
DROP FOREIGN KEY remote_ibfk_1,
DROP INDEX record,
DROP PRIMARY KEY,
ADD PRIMARY KEY(`id`);
ALTER TABLE `remote`
ADD CONSTRAINT remote_ibfk_1 FOREIGN KEY(record) REFERENCES `records`(id),
ADD INDEX remote_ibfk_2 (record),
COLLATE=utf8_general_ci;
ALTER TABLE `users`
DROP PRIMARY KEY,
ADD PRIMARY KEY(`id`),
CHANGE COLUMN password password varchar(255) NULL AFTER type,
ADD COLUMN backend varchar(50) NOT NULL AFTER name,
COLLATE=utf8_general_ci;
CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`type` varchar(10) NOT NULL,
`modified_at` int(11) NOT NULL,
`account` varchar(40) CHARACTER SET utf8 DEFAULT NULL,
`comment` text CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `cryptokeys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`flags` int(11) NOT NULL,
`active` tinyint(1) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `supermasters` (
`ip` varchar(64) NOT NULL,
`nameserver` varchar(255) NOT NULL,
`account` varchar(40) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`ip`, `nameserver`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `tsigkeys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`algorithm` varchar(50) DEFAULT NULL,
`secret` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
UPDATE options SET value=5 WHERE name='schema_version';

View file

@ -0,0 +1,3 @@
UPDATE users SET backend='native' WHERE backend='';
UPDATE options SET value=6 WHERE name='schema_version';

View file

@ -0,0 +1,10 @@
ALTER TABLE `remote`
DROP FOREIGN KEY remote_ibfk_1;
ALTER TABLE records MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT;
ALTER TABLE remote MODIFY record BIGINT;
ALTER TABLE `remote`
ADD CONSTRAINT remote_ibfk_1 FOREIGN KEY(record) REFERENCES `records`(id);
UPDATE options SET value=7 WHERE name='schema_version';

231
backend/src/sql/setup.sql Normal file
View file

@ -0,0 +1,231 @@
-- --------------------------------------------------------
--
-- Table structure for table `comments`
--
CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`type` varchar(10) NOT NULL,
`modified_at` int(11) NOT NULL,
`account` varchar(40) CHARACTER SET utf8 DEFAULT NULL,
`comment` text CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `cryptokeys`
--
CREATE TABLE `cryptokeys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`flags` int(11) NOT NULL,
`active` tinyint(1) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `domainmetadata`
--
CREATE TABLE `domainmetadata` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`kind` varchar(32) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `domains`
--
CREATE TABLE `domains` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`master` varchar(128) DEFAULT NULL,
`last_check` int(11) DEFAULT NULL,
`type` varchar(6) NOT NULL,
`notified_serial` int(10) UNSIGNED DEFAULT NULL,
`account` varchar(40) CHARACTER SET utf8 DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `permissions`
--
CREATE TABLE `permissions` (
`domain_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`domain_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `records`
--
CREATE TABLE `records` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`type` varchar(10) DEFAULT NULL,
`content` varchar(64000) DEFAULT NULL,
`ttl` int(11) DEFAULT NULL,
`prio` int(11) DEFAULT NULL,
`change_date` int(11) DEFAULT NULL,
`disabled` tinyint(1) DEFAULT '0',
`ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
`auth` tinyint(1) DEFAULT '1',
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `remote`
--
CREATE TABLE `remote` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`record` bigint(20) NOT NULL,
`description` varchar(255) NOT NULL,
`type` varchar(20) NOT NULL,
`security` varchar(2000) NOT NULL,
`nonce` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `supermasters`
--
CREATE TABLE `supermasters` (
`ip` varchar(64) NOT NULL,
`nameserver` varchar(255) NOT NULL,
`account` varchar(40) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`ip`, `nameserver`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `tsigkeys`
--
CREATE TABLE `tsigkeys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`algorithm` varchar(50) DEFAULT NULL,
`secret` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`backend` varchar(50) NOT NULL,
`type` varchar(20) NOT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `options`
--
CREATE TABLE `options` (
`name` varchar(255) NOT NULL,
`value` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Dumping data for table `options`
--
INSERT INTO `options` (`name`, `value`) VALUES
('schema_version', '7');
-- --------------------------------------------------------
--
-- Indexes for dumped tables
--
--
-- Indexes for table `comments`
--
ALTER TABLE `comments`
ADD KEY `comments_name_type_idx` (`name`,`type`),
ADD KEY `comments_order_idx` (`domain_id`,`modified_at`);
--
-- Indexes for table `cryptokeys`
--
ALTER TABLE `cryptokeys`
ADD KEY `domainidindex` (`domain_id`);
--
-- Indexes for table `domainmetadata`
--
ALTER TABLE `domainmetadata`
ADD KEY `domainmetadata_idx` (`domain_id`,`kind`);
--
-- Indexes for table `records`
--
ALTER TABLE `records`
ADD KEY `nametype_index` (`name`,`type`),
ADD KEY `domain_id` (`domain_id`),
ADD KEY `ordername` (`ordername`);
--
-- Indexes for table `tsigkeys`
--
ALTER TABLE `tsigkeys`
ADD UNIQUE KEY `namealgoindex` (`name`,`algorithm`);
--
-- Constraints for table `permissions`
--
ALTER TABLE `permissions`
ADD CONSTRAINT `permissions_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domains` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `permissions_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
--
-- Constraints for table `records`
--
ALTER TABLE `records`
ADD CONSTRAINT `records_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domains` (`id`) ON DELETE CASCADE;
--
-- Constraints for table `remote`
--
ALTER TABLE `remote`
ADD CONSTRAINT `remote_ibfk_1` FOREIGN KEY (`record`) REFERENCES `records` (`id`) ON DELETE CASCADE;

View file

@ -0,0 +1,37 @@
<?php
namespace Utils;
require '../vendor/autoload.php';
class PagingInfo
{
/** @var int */
public $page;
/** @var int */
public $pageSize;
/** @var int */
public $totalPages;
public function __construct(? int $page, ? int $pageSize, int $totalPages = null)
{
$this->page = $page === null ? 1 : $page;
$this->pageSize = $pageSize;
$this->totalPages = $totalPages;
}
public function toArray()
{
$val = [
'page' => $this->page,
'total' => $this->totalPages,
'pagesize' => $this->pageSize
];
return array_filter($val, function ($var) {
return !is_null($var);
});
}
}

396
backend/test/db.sql Normal file
View file

@ -0,0 +1,396 @@
-- phpMyAdmin SQL Dump
-- version 4.5.4.1deb2ubuntu2
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Erstellungszeit: 25. Dez 2019 um 23:22
-- Server-Version: 5.7.23-0ubuntu0.16.04.1
-- PHP-Version: 7.0.30-0ubuntu0.16.04.1
SET FOREIGN_KEY_CHECKS=0;
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
DROP TABLE IF EXISTS `comments`, `cryptokeys`, `domainmetadata`, `domains`, `options`, `permissions`, `records`, `remote`, `supermasters`, `tsigkeys`, `users`;
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Datenbank: `pdnsnew`
--
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `comments`
--
DROP TABLE IF EXISTS `comments`;
CREATE TABLE `comments` (
`id` int(11) NOT NULL,
`domain_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`type` varchar(10) NOT NULL,
`modified_at` int(11) NOT NULL,
`account` varchar(40) CHARACTER SET utf8 DEFAULT NULL,
`comment` text CHARACTER SET utf8 NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `cryptokeys`
--
DROP TABLE IF EXISTS `cryptokeys`;
CREATE TABLE `cryptokeys` (
`id` int(11) NOT NULL,
`domain_id` int(11) NOT NULL,
`flags` int(11) NOT NULL,
`active` tinyint(1) DEFAULT NULL,
`content` text
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `domainmetadata`
--
DROP TABLE IF EXISTS `domainmetadata`;
CREATE TABLE `domainmetadata` (
`id` int(11) NOT NULL,
`domain_id` int(11) NOT NULL,
`kind` varchar(32) DEFAULT NULL,
`content` text
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `domains`
--
DROP TABLE IF EXISTS `domains`;
CREATE TABLE `domains` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`master` varchar(128) DEFAULT NULL,
`last_check` int(11) DEFAULT NULL,
`type` varchar(6) NOT NULL,
`notified_serial` int(10) UNSIGNED DEFAULT NULL,
`account` varchar(40) CHARACTER SET utf8 DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `domains`
--
INSERT INTO `domains` (`id`, `name`, `master`, `last_check`, `type`, `notified_serial`, `account`) VALUES
(1, 'example.com', NULL, NULL, 'MASTER', NULL, NULL),
(2, 'slave.example.net', '12.34.56.78', NULL, 'SLAVE', NULL, NULL),
(3, 'foo.de', NULL, NULL, 'NATIVE', NULL, NULL),
(4, 'bar.net', NULL, NULL, 'MASTER', NULL, NULL),
(5, 'baz.org', NULL, NULL, 'MASTER', NULL, NULL);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `options`
--
DROP TABLE IF EXISTS `options`;
CREATE TABLE `options` (
`name` varchar(255) NOT NULL,
`value` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Daten für Tabelle `options`
--
INSERT INTO `options` (`name`, `value`) VALUES
('schema_version', '7');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `permissions`
--
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE `permissions` (
`domain_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Daten für Tabelle `permissions`
--
INSERT INTO `permissions` (`domain_id`, `user_id`) VALUES
(1, 2),
(2, 2);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `records`
--
DROP TABLE IF EXISTS `records`;
CREATE TABLE `records` (
`id` bigint(20) NOT NULL,
`domain_id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`type` varchar(10) DEFAULT NULL,
`content` varchar(64000) DEFAULT NULL,
`ttl` int(11) DEFAULT NULL,
`prio` int(11) DEFAULT NULL,
`change_date` int(11) DEFAULT NULL,
`disabled` tinyint(1) DEFAULT '0',
`ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
`auth` tinyint(1) DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `records`
--
INSERT INTO `records` (`id`, `domain_id`, `name`, `type`, `content`, `ttl`, `prio`, `change_date`, `disabled`, `ordername`, `auth`) VALUES
(1, 1, 'test.example.com', 'A', '12.34.56.78', 86400, 0, 1521645110, 0, NULL, 1),
(2, 1, 'sdfdf.example.com', 'TXT', 'foo bar baz', 60, 10, 1522321931, 0, NULL, 1),
(3, 1, 'foo.example.com', 'AAAA', '::1', 86400, 0, 1522321902, 0, NULL, 1),
(4, 3, 'foo.de', 'A', '9.8.7.6', 86400, 0, 1522321989, 0, NULL, 1),
(5, 1, 'example.com', 'SOA', 'ns1.example.com hostmaster.example.com 2018041300 3600 900 604800 86400', 86400, NULL, 1523629358, 0, NULL, 1);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `remote`
--
DROP TABLE IF EXISTS `remote`;
CREATE TABLE `remote` (
`id` int(11) NOT NULL,
`record` bigint(20) DEFAULT NULL,
`description` varchar(255) NOT NULL,
`type` varchar(20) NOT NULL,
`security` varchar(2000) NOT NULL,
`nonce` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Daten für Tabelle `remote`
--
INSERT INTO `remote` (`id`, `record`, `description`, `type`, `security`, `nonce`) VALUES
(1, 1, 'Password Test', 'password', '$2y$10$abocd6jj/Tw4jzDtqTnjreNzwcerzkXwoVc.JvZBoZ6p0grEKDWoW', NULL),
(2, 4, 'Key Test', 'key', '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5mu3aH90uSXY9sVLgVSz\nKj4FEctrpFDPyVC4ufbJa/44fuLABFe+IizgZUheNBBO7FjpLJYvsL24o6TEeht4\no5j0KHrRHXqp4WQuAL3ZREv/AhNaOC9/xyjoGwUkKkdC2bIfh0J/ACkezxvUrPsh\nbzhzY+co/M9PqlgTbjKjvlv/pRj2dSp98FzUme3HCh7Nn1EOM3yPMtaKNA9Qkkz1\noalfR3xmJjIanoS9zcK77/yyQ8VwI//CgxvnpnWbORZG0B9W2ZBoI8Bj4zprbbFG\nKNmrb403wfDijYF3MXpSMjKvJ5YVuZsn35EWIi5tqFc0oV7Ryy9nBHzKeoYN7Szs\nrXIS5+ZcQDLuN+pqJ7ByVaw4aVn85py8IdO0IYD5xeKd1i0iqm+KSoFTS1jiNSZu\n6iVl4odixWtW7oPLYBbd/vD2F7Ua5cLd12Rs+6kEVtlpnIf7txyFQL4QHYJxB7fI\ny+m70mfufVvKbFh/mHkhe+Arv71ERDMfAV3AD8++axLqYfU/LLFzanjwIBctAA9a\nj++G0lwl1adURwnBeq8+YrMU4/wg9efquKXLR40dU9nkMJOm5tPm+XHt4o3wio4X\n2FqnD57I7qJCWVc00HtpeWno5vHL+eJu0TdxjBuYXnQfwa1z9pWvGaoBtg7tyHgv\ng7YZJzF1MW5N9ZqnkdFJVEsCAwEAAQ==\n-----END PUBLIC KEY-----', NULL),
(3, 1, 'Key Test 2', 'key', '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrJ/UoQoN5rO1nwrWBNDr3TgPB\nkm6UmN/B6NY7RXcYTJOFEP6iWqTj9Pw8aT8/DSn2uTMeQK6kWNUAWmRaylQI2QHQ\ndPtrI6piTpjvKm+KbR+n3e4QJ/zOcg06cHYJJiyhPjfC12j3ZxINOV3LDbEKq4s0\nHxMGYZHPu+UezapeeQIDAQAB\n-----END PUBLIC KEY-----', NULL);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `supermasters`
--
DROP TABLE IF EXISTS `supermasters`;
CREATE TABLE `supermasters` (
`ip` varchar(64) NOT NULL,
`nameserver` varchar(255) NOT NULL,
`account` varchar(40) CHARACTER SET utf8 NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `tsigkeys`
--
DROP TABLE IF EXISTS `tsigkeys`;
CREATE TABLE `tsigkeys` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`algorithm` varchar(50) DEFAULT NULL,
`secret` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `users`
--
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`backend` varchar(50) NOT NULL,
`type` varchar(20) NOT NULL,
`password` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Daten für Tabelle `users`
--
INSERT INTO `users` (`id`, `name`, `backend`, `type`, `password`) VALUES
(1, 'admin', 'native', 'admin', '$2y$10$9iIDHWgjY0pEsz8pZLXPx.gkMNDxTMzb7U0Um5hUGjKmUUHWQNXcW'),
(2, 'user', 'native', 'user', '$2y$10$MktCI4XcfD0FpIFSkxex6OVifnIw3Nqw6QJueWmjVte99wx6XGBoq'),
(3, 'configuser', 'config', 'user', NULL);
--
-- Indizes der exportierten Tabellen
--
--
-- Indizes für die Tabelle `comments`
--
ALTER TABLE `comments`
ADD PRIMARY KEY (`id`),
ADD KEY `comments_name_type_idx` (`name`,`type`),
ADD KEY `comments_order_idx` (`domain_id`,`modified_at`);
--
-- Indizes für die Tabelle `cryptokeys`
--
ALTER TABLE `cryptokeys`
ADD PRIMARY KEY (`id`),
ADD KEY `domainidindex` (`domain_id`);
--
-- Indizes für die Tabelle `domainmetadata`
--
ALTER TABLE `domainmetadata`
ADD PRIMARY KEY (`id`),
ADD KEY `domainmetadata_idx` (`domain_id`,`kind`);
--
-- Indizes für die Tabelle `domains`
--
ALTER TABLE `domains`
ADD PRIMARY KEY (`id`);
--
-- Indizes für die Tabelle `options`
--
ALTER TABLE `options`
ADD PRIMARY KEY (`name`);
--
-- Indizes für die Tabelle `permissions`
--
ALTER TABLE `permissions`
ADD PRIMARY KEY (`domain_id`,`user_id`),
ADD KEY `permissions_ibfk_2` (`user_id`);
--
-- Indizes für die Tabelle `records`
--
ALTER TABLE `records`
ADD PRIMARY KEY (`id`),
ADD KEY `nametype_index` (`name`,`type`),
ADD KEY `domain_id` (`domain_id`),
ADD KEY `ordername` (`ordername`);
--
-- Indizes für die Tabelle `remote`
--
ALTER TABLE `remote`
ADD PRIMARY KEY (`id`),
ADD KEY `remote_ibfk_1` (`record`);
--
-- Indizes für die Tabelle `supermasters`
--
ALTER TABLE `supermasters`
ADD PRIMARY KEY (`ip`,`nameserver`);
--
-- Indizes für die Tabelle `tsigkeys`
--
ALTER TABLE `tsigkeys`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `namealgoindex` (`name`,`algorithm`);
--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT für exportierte Tabellen
--
--
-- AUTO_INCREMENT für Tabelle `comments`
--
ALTER TABLE `comments`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `cryptokeys`
--
ALTER TABLE `cryptokeys`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `domainmetadata`
--
ALTER TABLE `domainmetadata`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `domains`
--
ALTER TABLE `domains`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT für Tabelle `records`
--
ALTER TABLE `records`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT für Tabelle `remote`
--
ALTER TABLE `remote`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- AUTO_INCREMENT für Tabelle `tsigkeys`
--
ALTER TABLE `tsigkeys`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- Constraints der exportierten Tabellen
--
--
-- Constraints der Tabelle `permissions`
--
ALTER TABLE `permissions`
ADD CONSTRAINT `permissions_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domains` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `permissions_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
--
-- Constraints der Tabelle `records`
--
ALTER TABLE `records`
ADD CONSTRAINT `records_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domains` (`id`) ON DELETE CASCADE;
--
-- Constraints der Tabelle `remote`
--
ALTER TABLE `remote`
ADD CONSTRAINT `remote_ibfk_1` FOREIGN KEY (`record`) REFERENCES `records` (`id`);
SET FOREIGN_KEY_CHECKS=1;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

61
backend/test/package-lock.json generated Normal file
View file

@ -0,0 +1,61 @@
{
"name": "backend-test",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
},
"axios": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
}
},
"cartesian-product": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/cartesian-product/-/cartesian-product-2.1.2.tgz",
"integrity": "sha1-yahGLFSrGaDF/TIZKSLiOatMpP0="
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node-rsa": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-0.4.2.tgz",
"integrity": "sha1-1jkXKewWqDDtWjgEKzFX0tXXJTA=",
"requires": {
"asn1": "0.2.3"
}
}
}
}

10
backend/test/package.json Normal file
View file

@ -0,0 +1,10 @@
{
"name": "backend-test",
"version": "1.0.0",
"description": "Dependencies for pdnsmanager test",
"dependencies": {
"axios": "^0.18.1",
"cartesian-product": "^2.1.2",
"node-rsa": "^0.4.2"
}
}

134
backend/test/test.sh Executable file
View file

@ -0,0 +1,134 @@
#!/bin/bash
function makeConfig() {
source config.sh
touch "logfile.log"
cat <<EOM > "../src/config/ConfigOverride.php"
<?php
return [
'db' => [
'host' => '$DBHOST',
'user' => '$DBUSER',
'password' => '$DBPASSWORD',
'dbname' => '$DBNAME'
],
'logging' => [
'level' => 'error',
'path' => '../../test/logfile.log'
],
'authentication' => [
'native' => [
'plugin' => 'native',
'prefix' => 'default',
'config' => null
],
'config' => [
'plugin' => 'config',
'prefix' => 'config',
'config' => [
'configuser' => '\$2y\$10\$twlIJ0hYeaHqMsiM7OdLr.4HkV6/EEQneDg9uZiU.l7yn1bpxSD1.',
'notindb' => '\$2y\$10\$z1dD1Q5u68l5iqEmqnOAVuoR5VWR77HUfxMUycJ9TdDG3H5dLZGVW'
]
]
],
'proxys' => ['127.0.0.1']
];
EOM
}
function clearConfig() {
rm "../src/config/ConfigOverride.php"
rm "logfile.log"
}
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
source config.sh
cd "$SCRIPTPATH"
if [ $# -lt 1 ]
then
echo "The script needs either run or all as parameter."
exit 1
fi
if [ $1 == "run" ]
then
if [ $# -lt 2 ]
then
echo "run needs an argument."
exit 1
fi
makeConfig
echo "Preparing Database"
if [ -z "$DBPASSWORD" ]
then
mysql "-h$DBHOST" "-u$DBUSER" "$DBNAME" < db.sql
else
mysql "-h$DBHOST" "-u$DBUSER" "-p$DBPASSWORD" "$DBNAME" < db.sql
fi
echo "Executing test"
if ! node "tests/$2.js" "$TESTURL"
then
echo "Test failed"
clearConfig
exit 1
else
if [ $(cat logfile.log | wc -l) -gt 0 ]
then
echo "Errors in logfile:"
cat "logfile.log"
clearConfig
exit 2
else
echo "Test successfull"
clearConfig
exit 0
fi
fi
elif [ $1 == "all" ]
then
for test in tests/*
do
makeConfig
echo -n $(basename $test .js) "..."
if [ -z "$DBPASSWORD" ]
then
mysql "-h$DBHOST" "-u$DBUSER" "$DBNAME" < db.sql
else
mysql "-h$DBHOST" "-u$DBUSER" "-p$DBPASSWORD" "$DBNAME" < db.sql
fi
echo -n "..."
if ! node "$test" "$TESTURL"
then
clearConfig
exit 1
else
if [ $(cat logfile.log | wc -l) -gt 0 ]
then
cat "logfile.log"
clearConfig
exit 2
else
echo " OK"
fi
fi
clearConfig
done
else
echo "$1 is not a valid command."
exit 3
fi

76
backend/test/testlib.js Normal file
View file

@ -0,0 +1,76 @@
const assert = require('assert');
const axios = require('axios');
async function runTest(user, f) {
const assertObj = {
equal: assert.deepStrictEqual,
true: assert.ok
};
var requestObj = axios.create({
baseURL: process.argv[2],
validateStatus: function () { return true; }
});
try {
const token = await logIn(assertObj, requestObj, user);
requestObj = axios.create({
baseURL: process.argv[2],
validateStatus: function () { return true; },
headers: { 'X-Authentication': token }
});
await f(assertObj, requestObj);
await logOut(assertObj, requestObj, token);
} catch (e) {
if (e instanceof assert.AssertionError) {
console.log(e.toString());
console.log('\nExpected:');
console.log(e.expected);
console.log('\nGot:');
console.log(e.actual);
process.exit(2);
} else {
console.log(e.toString());
process.exit(1);
}
}
}
async function run(f) {
await f();
process.exit(0);
}
async function logIn(assert, req, username) {
//Try to login with valid username and password
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: username,
password: username
}
});
assert.equal(res.status, 201, 'LOGIN: Status not valid');
assert.equal(res.data.username, username, 'LOGIN: Username should be ' + username);
assert.equal(res.data.token.length, 86, 'LOGIN: Token length fail');
return res.data.token;
}
async function logOut(assert, req, token) {
//Try to logout check if this works
var res = await req({
url: '/sessions/' + token,
method: 'delete'
});
assert.equal(res.status, 204, 'LOGOUT: Answer should be successfull but empty');
}
module.exports = runTest;
module.exports.run = run;

View file

@ -0,0 +1,312 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test missing field
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test'
}
});
assert.equal(res.status, 422);
//Test invalid type
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test',
type: 'foo'
}
});
assert.equal(res.status, 400);
//Test missing key
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test',
type: 'key'
}
});
assert.equal(res.status, 422);
//Test missing password
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test',
type: 'password'
}
});
assert.equal(res.status, 422);
//Test invalid key
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test',
type: 'key',
key: 'foo'
}
});
assert.equal(res.status, 400);
//Test invalid record
var res = await req({
url: '/records/100/credentials',
method: 'post',
data: {
description: 'Test',
type: 'password',
password: 'foo'
}
});
assert.equal(res.status, 404, 'Not existent record should trigger error.');
//Add key (key is intensionally very short but valid) and get it
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test Key',
type: 'key',
key: '-----BEGIN PUBLIC KEY-----\nMDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMOLSxmtlYxSkEKep11gjq200PTKVUaA\nyalonAKxw3XnAgMBAAE=\n-----END PUBLIC KEY-----'
}
});
assert.equal(res.status, 201, 'Adding key should succeed.');
assert.equal(res.data, {
id: 4,
description: 'Test Key',
type: 'key',
key: '-----BEGIN PUBLIC KEY-----\nMDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMOLSxmtlYxSkEKep11gjq200PTKVUaA\nyalonAKxw3XnAgMBAAE=\n-----END PUBLIC KEY-----'
}, 'Adding credential data fail.');
var res = await req({
url: '/records/1/credentials/4',
method: 'get'
});
assert.equal(res.status, 200, 'Added key should be found.');
assert.equal(res.data, {
id: 4,
description: 'Test Key',
type: 'key',
key: '-----BEGIN PUBLIC KEY-----\nMDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMOLSxmtlYxSkEKep11gjq200PTKVUaA\nyalonAKxw3XnAgMBAAE=\n-----END PUBLIC KEY-----'
}, 'Added key does not match.');
//Add password and get it
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test Password',
type: 'password',
password: 'foo'
}
});
assert.equal(res.status, 201, 'Adding password should succeed.');
assert.equal(res.data, {
id: 5,
description: 'Test Password',
type: 'password',
}, 'Adding credential data fail.');
var res = await req({
url: '/records/1/credentials/5',
method: 'get'
});
assert.equal(res.status, 200, 'Added key should be found.');
assert.equal(res.data, {
id: 5,
description: 'Test Password',
type: 'password',
}, 'Added password does not match.');
//Update credential
var res = await req({
url: '/records/1/credentials/4',
method: 'put',
data: {
type: 'key',
key: '-----BEGIN PUBLIC KEY-----\nMDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMTyWha8C93l2NAPMkLPZ2WnbkqWXOnH\no3RenmVJHn1tAgMBAAE=\n-----END PUBLIC KEY-----'
}
});
assert.equal(res.status, 204, 'Updating record should succeed.');
var res = await req({
url: '/records/1/credentials/4',
method: 'get'
});
assert.equal(res.status, 200, 'Updated credential should be found.');
assert.equal(res.data, {
id: 4,
description: 'Test Key',
type: 'key',
key: '-----BEGIN PUBLIC KEY-----\nMDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMTyWha8C93l2NAPMkLPZ2WnbkqWXOnH\no3RenmVJHn1tAgMBAAE=\n-----END PUBLIC KEY-----'
}, 'Updated key does not match.');
// Change type to password
var res = await req({
url: '/records/1/credentials/4',
method: 'put',
data: {
description: 'Foo Bar',
type: 'password',
password: 'foo'
}
});
assert.equal(res.status, 204, 'Updating record should succeed.');
var res = await req({
url: '/records/1/credentials/4',
method: 'get'
});
assert.equal(res.status, 200, 'Updated credential should be found.');
assert.equal(res.data, {
id: 4,
description: 'Foo Bar',
type: 'password'
}, 'Added key does not match.');
//Test update fails
var res = await req({
url: '/records/1/credentials/4',
method: 'put',
data: {
type: 'foo'
}
});
assert.equal(res.status, 400, 'Invalid type should trigger error.');
var res = await req({
url: '/records/1/credentials/4',
method: 'put',
data: {
type: 'key',
key: 'foo'
}
});
assert.equal(res.status, 400, 'Invalid key should trigger error.');
var res = await req({
url: '/records/1/credentials/4',
method: 'put',
data: {
type: 'key'
}
});
assert.equal(res.status, 422, 'Missing key should trigger error.');
var res = await req({
url: '/records/1/credentials/4',
method: 'put',
data: {
type: 'password'
}
});
assert.equal(res.status, 422, 'Missing password should trigger error.');
var res = await req({
url: '/records/1/credentials/100',
method: 'put',
data: {
description: 'foo'
}
});
assert.equal(res.status, 404, 'Invalid credential should trigger error.');
//Delete entry
var res = await req({
url: '/records/1/credentials/4',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of entry should succeed.');
//Delete not existing entry
var res = await req({
url: '/records/1/credentials/100',
method: 'delete'
});
assert.equal(res.status, 404, 'Deletion of not existing entry should fail.');
//Delete entry via wrong record
var res = await req({
url: '/records/4/credentials/5',
method: 'delete'
});
assert.equal(res.status, 404, 'Deletion of entry via wrong record should fail.');
});
await test('user', async function (assert, req) {
//Add password with missing permissions
var res = await req({
url: '/records/4/credentials',
method: 'post',
data: {
description: 'Test Password',
type: 'password',
password: 'foo'
}
});
assert.equal(res.status, 403, 'Adding password should fail for missing permissions.');
//Add password with missing permissions
var res = await req({
url: '/records/1/credentials',
method: 'post',
data: {
description: 'Test Password',
type: 'password',
password: 'foo'
}
});
assert.equal(res.status, 201, 'Adding password should succeed for user.');
assert.equal(res.data, {
id: 6,
description: 'Test Password',
type: 'password',
}, 'Adding credential data fail.');
//Delete entry
var res = await req({
url: '/records/1/credentials/6',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of entry should succeed for user.');
//Delete entry without permission
var res = await req({
url: '/records/4/credentials/2',
method: 'delete'
});
assert.equal(res.status, 403, 'Deletion of entry without permission should fail.');
});
});

View file

@ -0,0 +1,70 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test query
var res = await req({
url: '/records/1/credentials',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 1,
description: 'Password Test',
type: 'password'
},
{
id: 3,
description: 'Key Test 2',
type: 'key'
}
], 'Result fail for ' + res.config.url);
//Test query
var res = await req({
url: '/records/4/credentials',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 2,
description: 'Key Test',
type: 'key'
}
], 'Result fail for ' + res.config.url);
});
await test('user', async function (assert, req) {
//Test query
var res = await req({
url: '/records/1/credentials',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 1,
description: 'Password Test',
type: 'password'
},
{
id: 3,
description: 'Key Test 2',
type: 'key'
}
], 'Result fail for ' + res.config.url);
//Test permissions
var res = await req({
url: '/records/4/credentials',
method: 'get'
});
assert.equal(res.status, 403, 'Request should fail without permissions.');
});
});

View file

@ -0,0 +1,167 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Try to set soa for non exitent domain
var res = await req({
url: '/domains/100/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 404, 'Updating SOA for not existing domain should fail');
//Try to set soa for slave domain
var res = await req({
url: '/domains/2/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 405, 'Updating SOA for slave domain should fail');
//Try to set soa with missing fields
var res = await req({
url: '/domains/2/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 422, 'Updating SOA with missing fields should fail.');
//Getting soa data from master zone without soa should fail
var res = await req({
url: '/domains/4/soa',
method: 'get'
});
assert.equal(res.status, 404, 'Not existing soa should trigger error');
//Getting soa data from slave zone should fail
var res = await req({
url: '/domains/2/soa',
method: 'get'
});
assert.equal(res.status, 404, 'Geting soa from slave should trigger error');
//Soa data for test
var soaData = {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
};
//Set soa for zone without one
var res = await req({
url: '/domains/1/soa',
method: 'put',
data: soaData
});
assert.equal(res.status, 204, 'Updating SOA for Zone without one should succeed.');
//Get the new soa
var res = await req({
url: '/domains/1/soa',
method: 'get'
});
assert.equal(res.status, 200, 'Getting soa should succeed.');
const firstSerial = res.data.serial;
delete res.data['serial'];
assert.equal(res.data, soaData, 'The set and get data should be equal');
//Soa data for update test
soaData = {
primary: 'ns2.example.com',
email: 'hostmasterFoo@example.com',
refresh: 3601,
retry: 901,
expire: 604801,
ttl: 86401
};
//Update soa with new values
var res = await req({
url: '/domains/1/soa',
method: 'put',
data: soaData
});
assert.equal(res.status, 204, 'Updating SOA for Zone should succeed.');
//Check if update suceeded
var res = await req({
url: '/domains/1/soa',
method: 'get'
});
assert.equal(res.status, 200, 'Getting updated soa should succeed.');
assert.true(firstSerial < res.data.serial, 'Serial value should increase with update');
delete res.data['serial'];
assert.equal(res.data, soaData, 'The set and get data should be equal after update');
});
await test('user', async function (assert, req) {
//Soa data for test
var soaData = {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
};
//Updating soa for domain with permissions should work
var res = await req({
url: '/domains/1/soa',
method: 'put',
data: soaData
});
assert.equal(res.status, 204, 'Updating SOA for Zone should succeed for user.');
//Get the updated soa
var res = await req({
url: '/domains/1/soa',
method: 'get'
});
assert.equal(res.status, 200, 'Getting soa should succeed for user.');
delete res.data['serial'];
assert.equal(res.data, soaData, 'The set and get data should be equal');
//Updating soa for domain with permissions should work
var res = await req({
url: '/domains/4/soa',
method: 'put',
data: soaData
});
assert.equal(res.status, 403, 'Updating SOA for Zone without permissions should fail.');
});
});

View file

@ -0,0 +1,279 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test missing fields
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'abc.de'
}
});
assert.equal(res.status, 422, 'Missing type filed should trigger error.');
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'abc.de',
type: 'SLAVE'
}
});
assert.equal(res.status, 422, 'Missing master field for SLAVE domain should trigger error.');
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'abc.de',
type: 'FOO'
}
});
assert.equal(res.status, 400, 'Invalid domain type should trigger error.');
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'foo.de',
type: 'MASTER'
}
});
assert.equal(res.status, 409, 'Existing domain should trigger error.');
//Test creation of master zone
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'master.de',
type: 'MASTER'
}
});
assert.equal(res.status, 201, 'Creation should be successfull');
assert.equal(res.data, {
id: 6,
name: 'master.de',
type: 'MASTER'
}, 'Creation result fail.')
//Test creation of native zone
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'native.de',
type: 'NATIVE'
}
});
assert.equal(res.status, 201, 'Creation should be successfull');
assert.equal(res.data, {
id: 7,
name: 'native.de',
type: 'NATIVE'
}, 'Creation result fail.')
//Test creation of slave zone
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'slave.de',
type: 'SLAVE',
master: '1.2.3.4'
}
});
assert.equal(res.status, 201, 'Creation should be successfull');
assert.equal(res.data, {
id: 8,
name: 'slave.de',
type: 'SLAVE',
master: '1.2.3.4'
}, 'Creation result fail.')
//Get master domain
var res = await req({
url: '/domains/6',
method: 'get'
});
assert.equal(res.status, 200, 'Domain access for master domain should be OK.');
assert.equal(res.data, {
id: 6,
name: 'master.de',
type: 'MASTER',
records: 0
}, 'Master domain data mismatch');
//Get native domain
var res = await req({
url: '/domains/7',
method: 'get'
});
assert.equal(res.status, 200, 'Domain access for native domain should be OK.');
assert.equal(res.data, {
id: 7,
name: 'native.de',
type: 'NATIVE',
records: 0
}, 'Native domain data mismatch');
//Get slave domain
var res = await req({
url: '/domains/8',
method: 'get'
});
assert.equal(res.status, 200, 'Domain access for slave domain should be OK.');
assert.equal(res.data, {
id: 8,
name: 'slave.de',
type: 'SLAVE',
records: 0,
master: '1.2.3.4'
}, 'Slave domain data mismatch');
//Update slave domain
var res = await req({
url: '/domains/8',
method: 'put',
data: {
master: '9.8.7.6'
}
});
assert.equal(res.status, 204, 'Slave update should return no content');
//Check if update succeded
var res = await req({
url: '/domains/8',
method: 'get'
});
assert.equal(res.status, 200, 'Slave domain should be accessible after update.');
assert.equal(res.data.master, '9.8.7.6', 'Slave update had no effect');
//Check if update fails for non existing domain
var res = await req({
url: '/domains/100',
method: 'put',
data: {
master: '9.8.7.6'
}
});
assert.equal(res.status, 404, 'Update on not existing domain should fail.');
//Check if update fails for master zone
var res = await req({
url: '/domains/1',
method: 'put',
data: {
master: '9.8.7.6'
}
});
assert.equal(res.status, 405, 'Update on master zone should fail.');
//Check if update fails for missing field
var res = await req({
url: '/domains/100',
method: 'put',
data: {
foo: 'bar'
}
});
assert.equal(res.status, 422, 'Update with missing master field should fail.');
//Delete not existing domain
var res = await req({
url: '/domains/100',
method: 'delete'
});
assert.equal(res.status, 404, 'Non existing domain deletion should be 404.');
//Delete existing domain
var res = await req({
url: '/domains/8',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of domain 8 should be successfull.');
});
await test('user', async function (assert, req) {
//Test insufficient privileges for add
var res = await req({
url: '/domains',
method: 'post',
data: {
name: 'foo.de'
}
});
assert.equal(res.status, 403, 'Domain creation should be forbidden for users.')
//Test insufficient privileges for delete
var res = await req({
url: '/domains/1',
method: 'delete'
});
assert.equal(res.status, 403, 'Domain deletion should be forbidden for users.');
//Test update for domain with permissions
var res = await req({
url: '/domains/2',
method: 'put',
data: {
master: '9.8.7.6'
}
});
assert.equal(res.status, 204, 'Update of slave zone should work if user has permissions.');
//Test insufficient permissions
var res = await req({
url: '/domains/3',
method: 'put',
data: {
master: '9.8.7.6'
}
});
assert.equal(res.status, 403, 'Update of slave zone should fail without permissions.');
//Test insufficient privileges for get
var res = await req({
url: '/domains/3',
method: 'get'
});
assert.equal(res.status, 403, 'Domain get for domain 3 should be forbidden.');
//Test privileges for get
var res = await req({
url: '/domains/1',
method: 'get'
});
assert.equal(res.status, 200, 'Domain access for domain 1 should be OK.');
assert.equal(res.data, {
id: 1,
name: 'example.com',
type: 'MASTER',
records: 3
}, 'Domain 3 data mismatch');
});
});

View file

@ -0,0 +1,134 @@
const test = require('../testlib');
const cartesianProduct = require('cartesian-product');
test.run(async function () {
await test('admin', async function (assert, req) {
//GET /domains?page=5&pagesize=10&query=foo&sort=id-asc,name-desc,type-asc,records-asc&type=MASTER
//Test sorting in all combinations
const sortCombinations = cartesianProduct([
['', 'id-asc', 'id-desc'],
['', 'name-asc', 'name-desc'],
['', 'type-asc', 'type-desc'],
['', 'records-asc', 'records-desc']
]);
for (list of sortCombinations) {
list = list.filter((str) => str.length > 0);
var sortQuery = list.join(',');
var res = await req({
url: '/domains?sort=' + sortQuery,
method: 'get'
});
assert.equal(res.status, 200);
var sortedData = res.data.results.slice();
sortedData.sort(function (a, b) {
for (sort of list) {
var spec = sort.split('-');
if (a[spec[0]] < b[spec[0]]) {
return spec[1] == 'asc' ? -1 : 1;
} else if (a[spec[0]] > b[spec[0]]) {
return spec[1] == 'asc' ? 1 : -1;
}
}
return 0;
});
assert.equal(res.data.results, sortedData, 'Sort failed for ' + res.config.url);
}
//Test paging
var res = await req({
url: '/domains?pagesize=3',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 1,
total: 2,
pagesize: 3
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 3, "Should be 3 results.");
var res = await req({
url: '/domains?pagesize=3&page=2',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 2,
total: 2,
pagesize: 3
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 2, "Should be 2 results.");
//Test query
var res = await req({
url: '/domains?query=.net&sort=id-asc',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 2,
name: 'slave.example.net',
type: 'SLAVE',
master: '12.34.56.78',
records: 0
},
{
id: 4,
name: 'bar.net',
type: 'MASTER',
records: 0
}
], 'Result fail for ' + res.config.url);
//Type filter
var res = await req({
url: '/domains?type=NATIVE',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 3,
name: 'foo.de',
type: 'NATIVE',
records: 1
}
], 'Result fail for ' + res.config.url);
});
await test('user', async function (assert, req) {
//Type filter
var res = await req({
url: '/domains',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK for user');
assert.equal(res.data.results, [
{
id: 1,
name: 'example.com',
type: 'MASTER',
records: 3
},
{
id: 2,
name: 'slave.example.net',
type: 'SLAVE',
master: '12.34.56.78',
records: 0
}
], 'Result fail for user on ' + res.config.url);
});
});

View file

@ -0,0 +1,111 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test paging
var res = await req({
url: '/users/2/permissions?pagesize=1&page=2',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 2,
total: 2,
pagesize: 1
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 1, "Should be 1 results.");
var res = await req({
url: '/users/2/permissions',
method: 'get'
});
assert.equal(res.status, 200, 'Get of permissions should be OK');
assert.equal(res.data.results, [
{
domainId: '1',
domainName: 'example.com'
},
{
domainId: '2',
domainName: 'slave.example.net'
}
], 'Get permissions result fail');
//Add permission with missing field
var res = await req({
url: '/users/2/permissions',
method: 'post',
data: {
foo: 100
}
});
assert.equal(res.status, 422, 'Add of permission should fail for missing field.');
//Add permission which exists
var res = await req({
url: '/users/2/permissions',
method: 'post',
data: {
domainId: 1
}
});
assert.equal(res.status, 204, 'Add of permission should succeed for existing permission.');
//Add permission which does not exist
var res = await req({
url: '/users/2/permissions',
method: 'post',
data: {
domainId: 3
}
});
assert.equal(res.status, 204, 'Add of permission should succeed for not existing permission.');
// Revoke the new permission
var res = await req({
url: '/users/2/permissions/3',
method: 'delete'
});
assert.equal(res.status, 204, 'Revoking should succeed');
// Revoke the new permission again
var res = await req({
url: '/users/2/permissions/3',
method: 'delete'
});
assert.equal(res.status, 404, 'Second revocation of the same permission should fail');
});
await test('user', async function (assert, req) {
var res = await req({
url: '/users/2/permissions',
method: 'get'
});
assert.equal(res.status, 403, 'Get of permissions should fail for user.');
var res = await req({
url: '/users/2/permissions',
method: 'post',
data: {
domainId: 100
}
});
assert.equal(res.status, 403, 'Add of permission should fail for user.');
var res = await req({
url: '/users/2/permissions/1',
method: 'delete'
});
assert.equal(res.status, 403, 'Revoke of permission should fail for user.');
});
});

View file

@ -0,0 +1,279 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test missing fields
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'foo.abc.de',
type: 'A'
}
});
assert.equal(res.status, 422, 'Missing fields should trigger error.');
//Test invalid record type
var res = await req({
url: '/records',
method: 'post',
data: {
name: "dns.example.com",
type: "FOOBARBAZ",
content: "1.2.3.4",
priority: 0,
ttl: 86400,
domain: 1
}
});
assert.equal(res.status, 400, 'Invalid record type should trigger error.');
//Test adding for slave zone
var res = await req({
url: '/records',
method: 'post',
data: {
name: "dns.example.com",
type: "A",
content: "1.2.3.4",
priority: 0,
ttl: 86400,
domain: 2
}
});
assert.equal(res.status, 404, 'Adding record for slave should trigger error.');
//Test adding for not existing zone
var res = await req({
url: '/records',
method: 'post',
data: {
name: "dns.example.com",
type: "A",
content: "1.2.3.4",
priority: 0,
ttl: 86400,
domain: 100
}
});
assert.equal(res.status, 404, 'Adding record to not existing domain should trigger error.');
//Test adding of record
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}
});
assert.equal(res.status, 201, 'Adding of record should succeed.');
assert.equal(res.data, {
id: 6,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Adding record return data fail.');
//Get not existing record
var res = await req({
url: '/records/100',
method: 'get'
});
assert.equal(res.status, 404, 'Get of not existing record should fail.');
//Get created record
var res = await req({
url: '/records/6',
method: 'get'
});
assert.equal(res.status, 200, 'Get of created record should succeed.');
assert.equal(res.data, {
id: 6,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Record data should be the same it was created with.');
//Update record
var res = await req({
url: '/records/6',
method: 'put',
data: {
name: 'foo.example.com'
}
});
assert.equal(res.status, 204, 'Updating record should succeed');
//Get updated record
var res = await req({
url: '/records/6',
method: 'get'
});
assert.equal(res.status, 200, 'Get updated record should succeed.');
assert.equal(res.data, {
id: 6,
name: 'foo.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Updated record has wrong data.');
//Delete not existing record
var res = await req({
url: '/records/100',
method: 'delete'
});
assert.equal(res.status, 404, 'Deletion of not existing record should fail.');
//Delete existing record
var res = await req({
url: '/records/6',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of existing record should succeed.');
});
await test('user', async function (assert, req) {
//Test insufficient privileges for add
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 3
}
});
assert.equal(res.status, 403, 'Adding of record should fail for user.');
//Test insufficient privileges for delete
var res = await req({
url: '/records/4',
method: 'delete'
});
assert.equal(res.status, 403, 'Deletion of record should fail for user.');
//Test insufficient privileges for update
var res = await req({
url: '/records/4',
method: 'put',
data: {
name: 'foo.example.com',
ttl: 60
}
});
assert.equal(res.status, 403, 'Updating record should succeed');
//Test adding of record
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}
});
assert.equal(res.status, 201, 'Adding of record should succeed.');
assert.equal(res.data, {
id: 7,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Adding record return data fail.');
//Get created record
var res = await req({
url: '/records/7',
method: 'get'
});
assert.equal(res.status, 200, 'Get of created record should succeed.');
assert.equal(res.data, {
id: 7,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Record data should be the same it was created with.');
//Update record
var res = await req({
url: '/records/7',
method: 'put',
data: {
name: 'foo.example.com',
ttl: 60
}
});
assert.equal(res.status, 204, 'Updating record should succeed');
//Get updated record
var res = await req({
url: '/records/7',
method: 'get'
});
assert.equal(res.status, 200, 'Get updated record should succeed.');
assert.equal(res.data, {
id: 7,
name: 'foo.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 60,
domain: 1
}, 'Updated record has wrong data.');
//Delete existing record
var res = await req({
url: '/records/7',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of existing record should succeed.');
});
});

View file

@ -0,0 +1,192 @@
const test = require('../testlib');
const cartesianProduct = require('cartesian-product');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test sorting in all combinations
const sortCombinations = cartesianProduct([
['', 'id-asc', 'id-desc'],
['', 'name-asc', 'name-desc'],
['', 'type-asc', 'type-desc'],
['', 'content-asc', 'content-desc'],
['', 'priority-asc', 'priority-desc'],
['', 'ttl-asc', 'ttl-desc'],
]);
for (list of sortCombinations) {
list = list.filter((str) => str.length > 0);
var sortQuery = list.join(',');
var res = await req({
url: '/records?sort=' + sortQuery,
method: 'get'
});
assert.equal(res.status, 200);
var sortedData = res.data.results.slice();
sortedData.sort(function (a, b) {
for (sort of list) {
var spec = sort.split('-');
if (a[spec[0]] < b[spec[0]]) {
return spec[1] == 'asc' ? -1 : 1;
} else if (a[spec[0]] > b[spec[0]]) {
return spec[1] == 'asc' ? 1 : -1;
}
}
return 0;
});
assert.equal(res.data.results, sortedData, 'Sort failed for ' + res.config.url);
assert.equal(res.data.results.filter((i) => i.type === 'SOA').length, 0, 'No soa should be in records');
}
//Test paging
var res = await req({
url: '/records?pagesize=2',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 1,
total: 2,
pagesize: 2
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 2, "Should be 2 results.");
var res = await req({
url: '/records?pagesize=2&page=2',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 2,
total: 2,
pagesize: 2
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 2, "Should be 2 results.");
//Test query name
var res = await req({
url: '/records?queryName=foo&sort=id-asc',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [{
id: 3,
name: 'foo.example.com',
type: 'AAAA',
content: '::1',
priority: 0,
ttl: 86400,
domain: 1
},
{
id: 4,
name: 'foo.de',
type: 'A',
content: '9.8.7.6',
priority: 0,
ttl: 86400,
domain: 3
},
], 'Result fail for ' + res.config.url);
//Type filter
var res = await req({
url: '/records?type=TXT,AAAA',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [{
id: 2,
name: 'sdfdf.example.com',
type: 'TXT',
content: 'foo bar baz',
priority: 10,
ttl: 60,
domain: 1
},
{
id: 3,
name: 'foo.example.com',
type: 'AAAA',
content: '::1',
priority: 0,
ttl: 86400,
domain: 1
}], 'Result fail for ' + res.config.url);
//Test query content
var res = await req({
url: '/records?queryContent=6&sort=id-asc',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 1,
name: 'test.example.com',
type: 'A',
content: '12.34.56.78',
priority: 0,
ttl: 86400,
domain: 1
},
{
id: 4,
name: 'foo.de',
type: 'A',
content: '9.8.7.6',
priority: 0,
ttl: 86400,
domain: 3
}
], 'Result fail for ' + res.config.url);
});
await test('user', async function (assert, req) {
//Type filter
var res = await req({
url: '/records',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK for user');
assert.equal(res.data.results, [
{
id: 1,
name: 'test.example.com',
type: 'A',
content: '12.34.56.78',
priority: 0,
ttl: 86400,
domain: 1
},
{
id: 2,
name: 'sdfdf.example.com',
type: 'TXT',
content: 'foo bar baz',
priority: 10,
ttl: 60,
domain: 1
},
{
id: 3,
name: 'foo.example.com',
type: 'AAAA',
content: '::1',
priority: 0,
ttl: 86400,
domain: 1
}
], 'Result fail for user on ' + res.config.url);
});
});

View file

@ -0,0 +1,102 @@
const test = require('../testlib');
const NodeRSA = require('node-rsa');
const privkey =
`-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCrJ/UoQoN5rO1nwrWBNDr3TgPBkm6UmN/B6NY7RXcYTJOFEP6i
WqTj9Pw8aT8/DSn2uTMeQK6kWNUAWmRaylQI2QHQdPtrI6piTpjvKm+KbR+n3e4Q
J/zOcg06cHYJJiyhPjfC12j3ZxINOV3LDbEKq4s0HxMGYZHPu+UezapeeQIDAQAB
AoGAGGkbgwFxhPIP7gOMJYBQhKMA0CPVV6YyC5LsswlmQfXx+EGDP56T89sl+mu8
VH7JJGInk0IAZnow7tr1gylmMJ0ir6KfDKZQG95tkFHwCVM3ZqUx/X8VAVuZT2mo
6ckAC7/ZrqORiFCNDC1kWgiaNj7GldvcbNOGUIBOkStgM4ECQQDVLWI/hO0fiPhT
QWVu+4md1NjSv9MZdaIdm+FEVKyTjN/j1fDLNFIguC24veYvsgKf2AyYAJqiAihz
RQWey38RAkEAzYmjjZuKmtsaUknZxmYVJwZlatvHv/3V2REa3UwhVXhgpbBGahav
khH8W5u4JJ/VUpX34wje8g/Gp2M6aCg46QJAGtux8jDMM1ntd4fYwMfeSc1kWAEl
FqMUfsiB9Dr610g7eRgeU2vPISIzWIBMfRvfasYsqAYDdX/yGrvKfnxDEQJAcTUr
aXbPfAXMVKCqm3Vkly8VsyrEtcHZBItAUb156rq3+OrDjfFa2MihR8/YOAv1ElzZ
wSoEqiz4TQABjpcA6QJAX1QXYhHQpjLj4UF+8TkZg93Zmd86W5CN/gXSTFJGrZ8M
3DOyePDIw1omSzyfvYa3Rbl/NL5BxFH6cURg++z8FA==
-----END RSA PRIVATE KEY-----`;
const key = new NodeRSA(privkey, 'pkcs1', { signingScheme: 'pkcs1-sha512' });
test.run(async function () {
await test('admin', async function (assert, req) {
// Test updating
var time = Math.floor(new Date() / 1000);
var res = await req({
url: '/remote/updatekey',
method: 'post',
data: {
record: 1,
content: 'foobarbaz',
time: time,
signature: key.sign('1foobarbaz' + time, 'base64')
}
});
assert.equal(res.status, 204, 'Update should succeed');
var res = await req({
url: '/records/1',
method: 'get'
});
assert.equal(res.data.content, 'foobarbaz', 'Updating should change content.');
var res = await req({
url: '/remote/updatekey',
method: 'post',
data: {
record: 1,
content: 'foobarbaz',
time: time,
signature: key.sign('1foobarbazdef' + time, 'base64')
}
});
assert.equal(res.status, 403);
// Test not existing record
var res = await req({
url: '/remote/updatekey',
method: 'post',
data: {
record: 100,
content: 'foobarbaz',
time: time,
signature: key.sign('1foobarbazdef' + time, 'base64')
}
});
assert.equal(res.status, 404, 'Not existing record should trigger error');
// Test missing fields
var res = await req({
url: '/remote/updatekey',
method: 'post',
data: {
record: 100,
signature: key.sign('1foobarbazdef' + time, 'base64')
}
});
assert.equal(res.status, 422, 'Missing field should fail');
// Test wrong time
var time = Math.floor(new Date() / 1000) - 60;
var res = await req({
url: '/remote/updatekey',
method: 'post',
data: {
record: 1,
content: 'foobarbaz',
time: time,
signature: key.sign('1foobarbaz' + time, 'base64')
}
});
assert.equal(res.status, 403, 'Wrong time should fail');
});
});

View file

@ -0,0 +1,36 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
// Test updating
var res = await req({
url: '/remote/updatepw?record=1&content=foobarbaz&password=test',
method: 'get'
});
assert.equal(res.status, 204);
var res = await req({
url: '/records/1',
method: 'get'
});
assert.equal(res.data.content, 'foobarbaz', 'Updating should change content.');
// Test updating with invalid password
var res = await req({
url: '/remote/updatepw?record=1&content=foobarbaz&password=foo',
method: 'get'
});
assert.equal(res.status, 403);
// Test updating non existing record
var res = await req({
url: '/remote/updatepw?record=100&content=foobarbaz&password=foo',
method: 'get'
});
assert.equal(res.status, 404);
});
});

View file

@ -0,0 +1,46 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
var res = await req({
url: '/remote/ip',
method: 'get'
});
assert.equal(res.status, 200);
assert.equal(res.data, { ip: '127.0.0.1' }, 'No proxy header should return tcp client ip.');
var res = await req({
url: '/remote/ip',
method: 'get',
headers: {
'X-Forwarded-For': '1.2.3.4, 127.0.0.1'
}
});
assert.equal(res.status, 200);
assert.equal(res.data, { ip: '1.2.3.4' }, 'X-Forwarded-For Test 1');
var res = await req({
url: '/remote/ip',
method: 'get',
headers: {
'X-Forwarded-For': '4.3.2.1, 1.2.3.4, 127.0.0.1'
}
});
assert.equal(res.status, 200);
assert.equal(res.data, { ip: '1.2.3.4' }, 'X-Forwarded-For Test 2');
var res = await req({
url: '/remote/ip',
method: 'get',
headers: {
'X-Forwarded-For': '4.3.2.1, 1.2.3.4'
}
});
assert.equal(res.status, 200);
assert.equal(res.data, { ip: '1.2.3.4' }, 'X-Forwarded-For Test 3');
});
});

View file

@ -0,0 +1,15 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
var res = await req({
url: '/remote/servertime',
method: 'get'
});
const curTime = Math.floor(new Date() / 1000);
assert.equal(res.status, 200);
assert.true(Math.abs(curTime - res.data.time) < 2, 'Returned time is not within tolerance!');
});
});

View file

@ -0,0 +1,76 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Try to login with invalid username and password
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'foo',
password: 'bar'
}
});
assert.equal(res.status, 403, 'Status not valid');
//Try to login with invalid username
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'foo',
password: 'admin'
}
});
assert.equal(res.status, 403, 'Status not valid');
//Try to login with invalid password
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'admin',
password: 'foo'
}
});
assert.equal(res.status, 403, 'Status not valid');
//Try to login with missing field
var res = await req({
url: '/sessions',
method: 'post',
data: {
password: 'admin'
}
});
assert.equal(res.status, 422, 'Status not valid');
//Try to login with prefix
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'config/configuser',
password: 'configuser'
}
});
assert.equal(res.status, 201, 'Status not valid');
//Try to login with prefix but no db entry
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'config/notindb',
password: 'notindb'
}
});
assert.equal(res.status, 201, 'Status not valid');
});
});

View file

@ -0,0 +1,231 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test missing fields
var res = await req({
url: '/users',
method: 'post',
data: {
name: 'newadmin',
type: 'admin'
}
});
assert.equal(res.status, 422, 'Missing fields should trigger error.');
//Test invalid type
var res = await req({
url: '/users',
method: 'post',
data: {
name: 'newadmin',
type: 'foo',
password: 'foo'
}
});
assert.equal(res.status, 400, 'Invalid type should trigger error.');
//Test duplicate user
var res = await req({
url: '/users',
method: 'post',
data: {
name: 'admin',
type: 'admin',
password: 'foo'
}
});
assert.equal(res.status, 409, 'Duplicate user should trigger error.');
//Test user creation
var res = await req({
url: '/users',
method: 'post',
data: {
name: 'newadmin',
type: 'admin',
password: 'newadmin'
}
});
assert.equal(res.status, 201, 'User creation should succeed.');
assert.equal(res.data, { id: 4, name: 'newadmin', type: 'admin' }, 'Add user data fail.');
//Test if new user can log in
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'newadmin',
password: 'newadmin'
}
});
assert.equal(res.status, 201, 'Login with new user should succeed.');
//Test user get
var res = await req({
url: '/users/4',
method: 'get'
});
assert.equal(res.status, 200, 'New user should be found.');
assert.equal(res.data, { id: 4, name: 'newadmin', type: 'admin', native: true }, 'New user data fail.');
//Test user change without data
var res = await req({
url: '/users/4',
method: 'put',
data: { dummy: 'foo' }
});
assert.equal(res.status, 204, 'Update without field should succeed.');
//Test user get
var res = await req({
url: '/users/4',
method: 'get'
});
assert.equal(res.status, 200, 'New user should be found after update.');
assert.equal(res.data, { id: 4, name: 'newadmin', type: 'admin', native: true }, 'New user should not change by noop update.');
//Test user update
var res = await req({
url: '/users/4',
method: 'put',
data: {
name: 'foo1',
password: 'bar',
type: 'user'
}
});
assert.equal(res.status, 204, 'Update should succeed.');
//Test if updated user can log in
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'foo1',
password: 'bar'
}
});
assert.equal(res.status, 201, 'Login with updated user should succeed.');
//Test user update without password
var res = await req({
url: '/users/4',
method: 'put',
data: {
name: 'foo',
type: 'user'
}
});
assert.equal(res.status, 204, 'Update should succeed.');
//Test if updated user can log in
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'foo',
password: 'bar'
}
});
assert.equal(res.status, 201, 'Login with updated user should succeed.');
//Test user get
var res = await req({
url: '/users/4',
method: 'get'
});
assert.equal(res.status, 200, 'New user should be found after second update.');
assert.equal(res.data, { id: 4, name: 'foo', type: 'user', native: true }, 'New user should change by update.');
//Test user update conflict
var res = await req({
url: '/users/4',
method: 'put',
data: {
name: 'admin'
}
});
assert.equal(res.status, 409, 'Update with existent name should fail.');
//Test user delete for not existing user
var res = await req({
url: '/users/100',
method: 'delete'
});
assert.equal(res.status, 404, 'Deletion of not existens user should fail.');
//Test user delete
var res = await req({
url: '/users/4',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of user should succeed.');
var res = await req({
url: '/users/4',
method: 'get'
});
assert.equal(res.status, 404, 'New user should not be found after deletion.');
// Test me alias get
var res = await req({
url: '/users/me',
method: 'get'
});
assert.equal(res.status, 200, 'Admin should be able to use /me.');
assert.equal(res.data, { id: 1, name: 'admin', type: 'admin', native: true }, 'Admin /me data fail.');
// Test me alias update
var res = await req({
url: '/users/me',
method: 'put',
data: {
password: 'abc'
}
});
assert.equal(res.status, 204, 'Admin should be able to update /me.');
//Test if updated user can log in
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'admin',
password: 'abc'
}
});
assert.equal(res.status, 201, 'Login with updated admin should succeed.');
});
await test('user', async function (assert, req) {
// Test me alias get
var res = await req({
url: '/users/me',
method: 'get'
});
assert.equal(res.status, 200, 'User should be able to use /me.');
assert.equal(res.data, { id: 2, name: 'user', type: 'user', native: true }, 'User /me data fail.');
// Test me alias update
var res = await req({
url: '/users/me',
method: 'put',
data: {
password: 'abc'
}
});
assert.equal(res.status, 204, 'User should be able to update /me.');
//Test if updated user can log in
var res = await req({
url: '/sessions',
method: 'post',
data: {
username: 'user',
password: 'abc'
}
});
assert.equal(res.status, 201, 'Login with updated user should succeed.');
});
});

View file

@ -0,0 +1,137 @@
const test = require('../testlib');
const cartesianProduct = require('cartesian-product');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test sorting in all combinations
const sortCombinations = cartesianProduct([
['', 'id-asc', 'id-desc'],
['', 'name-asc', 'name-desc'],
['', 'type-asc', 'type-desc'],
]);
for (list of sortCombinations) {
list = list.filter((str) => str.length > 0);
var sortQuery = list.join(',');
var res = await req({
url: '/users?sort=' + sortQuery,
method: 'get'
});
assert.equal(res.status, 200);
var sortedData = res.data.results.slice();
sortedData.sort(function (a, b) {
for (sort of list) {
var spec = sort.split('-');
if (a[spec[0]] < b[spec[0]]) {
return spec[1] == 'asc' ? -1 : 1;
} else if (a[spec[0]] > b[spec[0]]) {
return spec[1] == 'asc' ? 1 : -1;
}
}
return 0;
});
assert.equal(res.data.results, sortedData, 'Sort failed for ' + res.config.url);
}
//Test paging
var res = await req({
url: '/users?pagesize=2',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 1,
total: 2,
pagesize: 2
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 2, "Should be 2 results.");
var res = await req({
url: '/users?pagesize=2&page=2',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.paging, {
page: 2,
total: 2,
pagesize: 2
}, 'Paging data fail for ' + res.config.url);
assert.equal(res.data.results.length, 1, "Should be 2 results.");
//Test query name
var res = await req({
url: '/users?query=user&sort=id-asc',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 2,
name: 'user',
type: 'user',
native: true
},
{
id: 3,
name: 'config/configuser',
type: 'user',
native: false
}
], 'Result fail for ' + res.config.url);
//Type filter
var res = await req({
url: '/users?type=admin,user',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results.length, 3, 'Result fail for ' + res.config.url);
//Type filter
var res = await req({
url: '/users?type=admin',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{
id: 1,
name: 'admin',
type: 'admin',
native: true
}
], 'Result fail for ' + res.config.url);
//Query all check for format
var res = await req({
url: '/users?sort=id-asc',
method: 'get'
});
assert.equal(res.status, 200, 'Status should be OK');
assert.equal(res.data.results, [
{ id: 1, name: 'admin', type: 'admin', native: true },
{ id: 2, name: 'user', type: 'user', native: true },
{ id: 3, name: 'config/configuser', type: 'user', native: false }
], 'Result fail for ' + res.config.url);
});
await test('user', async function (assert, req) {
//Type filter
var res = await req({
url: '/users',
method: 'get'
});
assert.equal(res.status, 403, 'Get should fail for user');
});
});

Some files were not shown because too many files have changed in this diff Show more