Compare commits

...

284 Commits

Author SHA1 Message Date
2754fb8144 revert css to customised but older
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-04 15:02:21 +10:00
4650a971a3 css update
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-04 14:54:05 +10:00
8cd1292a41 update packages
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-04 14:46:39 +10:00
ea5198a5b9 bugfix
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-11 21:01:06 +10:00
d000469836 golang updates
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-18 11:08:55 +10:00
02061f5b26 reduce logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-19 13:52:08 +10:00
ea3e8ddfbc re-add cache rebuild
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-05 09:47:54 +10:00
8182b899cf upgrade dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-05 09:45:12 +10:00
ae5b864feb update for new dell site
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-15 08:24:05 +10:00
4a6a7270f9 improve input checking when retrieving secret
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-08 08:36:20 +10:00
a7beb94341 ftps doesnt seem t owork
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-08 14:06:37 +10:00
4a0e98bab8 test secure ftp
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-08 13:59:00 +10:00
66a1917e6f return updated permission details in UpdatePermissionHandler
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-04 08:56:14 +11:00
526161f6b4 bugfix permission delete when only permissionId specified
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-03 11:10:36 +11:00
ff16acc816 add support to update permissions
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-03 10:42:11 +11:00
ee822b5c9d if username in UPN format for login try searching both user and full UPN string
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-02 16:55:11 +11:00
e427184310 test logic fix for ldap users not in an ldap group
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-02 15:11:24 +11:00
5719ce8f5d creating ldap user was not setting ldapuser flag
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-02 14:41:15 +11:00
a78f2b7c88 improve adding ldap user
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-02 14:13:21 +11:00
1171a7bbaa update readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-13 11:26:55 +11:00
8ff92e206e include count in listsecrets
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:33:08 +11:00
19ffc9e683 log schema
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:23:23 +11:00
968bcf1b7a fix sql statement
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:22:14 +11:00
77d063867a set schema version better
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:20:52 +11:00
c82bffe421 try using schema version better
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:17:38 +11:00
b801563074 cleanup
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:11:08 +11:00
4bc430633e fix audit message
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 12:09:02 +11:00
69a25fbb09 add username to secret retrieve audit log
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 11:48:11 +11:00
840b9f4863 include go version
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 10:06:14 +11:00
5b87ef0d30 whoops
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 10:04:08 +11:00
bfc734a6d1 update golang version
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-23 10:03:33 +11:00
b5c9b5ce19 more audit logs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 19:05:26 +11:00
9f0dafd4fd remove artificial restriction on multiple secret retrieval
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 16:27:06 +11:00
abaa291a14 add search by username
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 16:13:35 +11:00
de1a076d64 update docs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 11:22:57 +11:00
1b5a2e89dd db fix
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 11:01:38 +11:00
8799f0f796 add client IP to audit logs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 10:56:37 +11:00
317e0ab83d bugfix
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 10:40:27 +11:00
116a9e827b fix extra comma
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 09:21:44 +11:00
2ab6240a24 add lastlogin for user
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 09:17:31 +11:00
bb3bf3093d add missing secretId to audit logs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-17 14:40:36 +11:00
1d1aa098a9 change eventlog message
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-17 14:38:24 +11:00
f68bd9637d add event log retrieval
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-17 12:20:01 +11:00
5f63ee235b remove some logging containing hashes
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:26:04 +11:00
092fe32baf missed one
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:21:38 +11:00
44d3bc71ed remove extra debug statements
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:20:46 +11:00
dc3c5d1068 test new PrintStructContents
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:18:46 +11:00
d834a5c362 blah
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:16:37 +11:00
9e0c1e7cd7 blah
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:12:36 +11:00
9ac729b684 more testing
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:10:26 +11:00
7f43662cbc try debugging the debugging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:05:48 +11:00
b35d365467 test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:02:17 +11:00
77d487c1ce blah
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 17:00:53 +11:00
f5827ef432 debug
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:58:37 +11:00
6e1a28d2df try this
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:55:28 +11:00
5c3b2e19cf another fix
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:49:37 +11:00
e109cd084d try again to fix lastupdated
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:40:51 +11:00
f29c733080 fix LastUpdated default value
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:35:28 +11:00
97cd75b0d7 fix lastupdated definition
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:22:45 +11:00
b278a3c7d8 add last updated tracking for secrets
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:21:41 +11:00
498dd9a8c3 allow use of secretId when performing operations on secrets
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 16:08:40 +11:00
c99ffa8368 resume dell deploy
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 15:19:30 +11:00
8fec84c118 another fix
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 15:18:42 +11:00
2c63ea9632 try again
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-01-16 15:11:10 +11:00
12bfa69cce more re-arranging of db schema setup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-01-16 15:07:35 +11:00
f39a06dfd6 re-arrange initial database setup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-01-16 14:51:13 +11:00
65af8a7194 more audit records
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-01-16 12:02:23 +11:00
f6a2544839 more audit records
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-15 11:25:45 +11:00
e71c9e6df8 audit table should use UTC time
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-15 11:01:41 +11:00
db63b89c61 fix column name
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-15 10:55:57 +11:00
95125b691d test audit add
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-15 09:54:42 +11:00
5920db48d8 update docs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-12 15:09:21 +11:00
1a297464fe updates
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-12 13:24:46 +11:00
083fb0ebe1 allow user to move secret between safes
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-12 12:55:49 +11:00
a3333cebb6 update README
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-12 09:41:26 +11:00
d087492c31 re-arrange checks
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 18:02:25 +11:00
62606cbee5 more checking when creating ldap group
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 18:00:41 +11:00
b65d1ef52e remove dead code
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 17:39:53 +11:00
e59b9eefc2 improvement
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 17:31:27 +11:00
a85ed3fad8 check ldap group membership when logging in subsequently
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 16:48:22 +11:00
bf1174bd0f try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 16:12:58 +11:00
17aa04c1ea return secret when creating it
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 15:58:33 +11:00
91143f2628 temporarily remove condition to rebuild cache
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 14:49:25 +11:00
1a90963851 try doing magic with generics
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 14:48:08 +11:00
afec665759 add permission definition
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 14:32:50 +11:00
1ffa19d225 work on read-only restrictions
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 12:10:43 +11:00
1bd832f839 add checks for readonly access when update/delete secret
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 12:07:46 +11:00
eb5707a376 initial permissions endpoint implementation
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 11:34:43 +11:00
b77e47ba7e test returning created safe details
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 09:39:11 +11:00
73c487fd3d different test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 09:35:19 +11:00
f467304e9e fix log message
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 09:27:45 +11:00
06bbe010dd add route
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 09:26:48 +11:00
03cb298618 debugging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-11 09:25:18 +11:00
e14f4007a4 implement safe deletion
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 12:00:00 +11:00
cef741f2d8 fix GET to POST
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 11:46:44 +11:00
7b11958658 safe add handler
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 11:43:57 +11:00
0273f62611 fix POST to GET
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 11:35:22 +11:00
4b44c93693 update safe handler
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 11:33:07 +11:00
50f078db7a initial safes handler
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 11:29:21 +11:00
8143470b5a enable gin route
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 10:45:46 +11:00
b828416811 implement group delete
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 10:43:38 +11:00
f8000b749e replace LdapDN with LdapDn
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 10:21:58 +11:00
1c3307d43d fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 10:13:05 +11:00
eaeba09078 add group implementation
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 10:09:01 +11:00
0899b07d47 work on adding group support
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-10 09:16:52 +11:00
48611b22c9 fix json fields for ListSecret
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:53:07 +11:00
ed6966eab4 try fixing json key names in secret retrieve endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:47:56 +11:00
341d5961f0 more tidy up
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:40:04 +11:00
32512f3c04 remove some dead code
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:37:31 +11:00
25510c63e5 test storesecret update
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:34:41 +11:00
fc736df4e3 another test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:22:27 +11:00
80cb06a64c fix log instead of fmt
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:19:05 +11:00
4a62b9db25 more testing
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:17:55 +11:00
7e5be4e74f minor tweaks
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:10:59 +11:00
9c2ff979fb one more test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:01:12 +11:00
391d806c13 test getting UserId
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:58:38 +11:00
07ae9cf2ac fix audit table definition
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:54:10 +11:00
1b1ac50a61 fix bug with UserName
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:45:45 +11:00
43aabee7e8 remove roleid from Permission model
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:40:13 +11:00
5a87ecebfc test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:38:33 +11:00
203a564b04 logic fix
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:34:47 +11:00
b4355ee913 more logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:30:14 +11:00
d86ce64ea7 more logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:25:05 +11:00
dc9ffceb3e verify unlock key against hash on disk
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 21:22:16 +11:00
1bb983b4b9 try ToC
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 17:01:54 +11:00
d8b68f2815 one more update
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:56:00 +11:00
43e0ecd1ce i think it works now!
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:53:51 +11:00
e31c6e5c78 getting closer
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:50:13 +11:00
e57bfbdffe try other things
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:47:48 +11:00
c131674227 test3
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:46:16 +11:00
a7d839e712 test2
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:44:36 +11:00
6bd65d1261 test1
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:43:41 +11:00
07f5f6ee83 try more debugging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:40:10 +11:00
b5a28c0d39 try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:31:39 +11:00
f7414629bc retry
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:20:05 +11:00
3c8d18afc4 try something else
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:17:36 +11:00
a8b645288b temp disable user union
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:06:22 +11:00
54a8c1504e try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 16:03:40 +11:00
22e039e035 we do want to provide SecretId in output
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:57:26 +11:00
77979b1839 correct missing SafeId in listing
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:56:47 +11:00
db26c12483 try to retrieve UserName from secrets table
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:54:15 +11:00
123925e304 remove restriction
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:46:43 +11:00
b57a4ed95c try to improve ListSecrets
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:44:30 +11:00
13d5df2953 try to avoid SQLITE_BUSY error
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:36:46 +11:00
00f87ccb71 improve README
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:28:38 +11:00
43fa0b02aa remove unneeded check
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:10:11 +11:00
6423d83949 implement delete secret
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 15:08:11 +11:00
90da2367be combine sql query for user and group permissions
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 14:52:03 +11:00
92dcd67381 initial delete secret implementation
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 13:33:51 +11:00
f45a0f3aeb test actually updating secret
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 12:16:02 +11:00
bc1ca7b481 re-test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 12:06:20 +11:00
1c3d82ffba add Permission to struct
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:53:38 +11:00
3186fe9ebc update
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:51:21 +11:00
a4a25164d0 getting closer
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:49:59 +11:00
bc1021cd12 more debug
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:47:23 +11:00
4b62b75c78 update
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:45:34 +11:00
89bd1acc32 fix middleware logic error
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:42:44 +11:00
17fc1f2e66 re-test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:40:26 +11:00
5534347be7 try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:36:49 +11:00
e5764553d8 test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:30:13 +11:00
143e402dd6 still trying
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:22:12 +11:00
309d9c8fa3 try changing query
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:17:32 +11:00
c80ccd47bf ambiguous column name: UserName
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:11:23 +11:00
bc3591a690 fix query
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 11:04:40 +11:00
07fd43bf33 work on determining which secrets accessible to user
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 10:59:27 +11:00
7363936cd5 getting there
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 09:57:16 +11:00
1d9240456d test again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 09:56:01 +11:00
e89182c2d9 re-test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 09:54:23 +11:00
dbc2276d68 test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 09:51:32 +11:00
20dc745a64 test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 21:13:04 +11:00
598b9442c0 test
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 17:12:39 +11:00
c9a161ca25 more logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 17:03:05 +11:00
8cf05b6858 add debugging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 16:29:03 +11:00
affe9021aa try getting ListSecret to work again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 16:04:01 +11:00
46567ab1a4 try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:54:05 +11:00
fdfde7af0b handle roleid FK in secrets table
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:49:06 +11:00
1a2b6e5b41 fix RoleId in secrets table
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:45:08 +11:00
ac60d1daef updates
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:35:13 +11:00
d1857f2db1 fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:16:41 +11:00
0c232178db fix missing bracket
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:12:25 +11:00
586f275c91 debug
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:06:29 +11:00
fe502e150f more debugging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 15:02:24 +11:00
85f96a31d2 add debugging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 14:56:21 +11:00
fd626a9cbe default to groupid 2 when registering
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 14:47:52 +11:00
d1eecc5c4f new schema initial commit
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 14:30:16 +11:00
7ecf27f7dc try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 10:30:51 +11:00
c181095197 handle existing secrets with no safe id
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 10:27:04 +11:00
20b38677f4 test adding safeid with FK
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 10:18:56 +11:00
d5c50c146f remove pesky trailing comma
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 10:11:51 +11:00
9a107bc4ba more testing
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 10:10:19 +11:00
d8da3027e2 try sqlite transaction FK removal
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 10:04:26 +11:00
71c5de3762 fix sql typo
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 09:58:23 +11:00
04bf8270bb test schema update
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-08 09:54:57 +11:00
aba655cd3b update readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 14:07:16 +11:00
d45e61f59e avoid unnecessary ldap bind for first user login
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 11:45:35 +11:00
cdbf490b68 disable extra logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 11:28:34 +11:00
675744ad74 fix path of static file
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 11:26:38 +11:00
38076b6126 debug favicon
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 11:23:36 +11:00
e82ce9009f add favicons
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 11:21:57 +11:00
7bc20231ec exclude go cache folders from dell upload
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 10:58:53 +11:00
50b512e08e re-enable dell upload
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-05 10:53:57 +11:00
7f40884115 improve ldap login
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 10:33:09 +11:00
63cfe1fd8d try again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 10:19:46 +11:00
b51468db8c store ldap user in database
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 10:14:54 +11:00
cb7376eeeb progress on ldap
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-05 09:48:48 +11:00
fa4f896093 fix logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 21:14:27 +11:00
5c3f31224a use sAMAccountName instead of CN
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 21:09:40 +11:00
4409f8e2ff try retrieving groups of user
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 21:04:23 +11:00
217eebead8 don't defer ldaps close
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 21:00:14 +11:00
8068ddc0b2 handle ldap usernames that don't include domain name
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 20:57:05 +11:00
ffa8778d2b more work on ldap integration
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 20:45:34 +11:00
44b92a8b08 change search scope
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 16:26:22 +11:00
3d47ccd746 turn off ldap debug
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 16:20:53 +11:00
ad93b59769 more logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 16:07:54 +11:00
f57f11d855 more logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 15:59:36 +11:00
7a8fd8e200 work on LDAP
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 15:53:21 +11:00
d6c082675e fix user struct
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:41:14 +11:00
ead1340659 go detective work
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:36:49 +11:00
25414aada0 try different cache volume
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:15:16 +11:00
3feda1d102 add debug
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:13:14 +11:00
f5fc5c0a56 change cache key
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:08:55 +11:00
2b331719b9 add ldap columns to user table
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:07:59 +11:00
4e0e55473a whoops
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-04 15:00:22 +11:00
5a70d0f27a and again
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-04 14:57:36 +11:00
42e15c7176 change cache root
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-04 14:56:41 +11:00
53e48a50ac re-test
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-04 14:55:18 +11:00
5ded1b3696 test again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 14:42:35 +11:00
5e52813111 troubleshoot go cache location
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 14:36:10 +11:00
023fdc22a6 update
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 12:59:01 +11:00
ea70e073ec test build with cache
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 12:27:22 +11:00
55f7bacd7b test again
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 12:23:08 +11:00
2398288e08 use log rather than fmt
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 12:02:05 +11:00
f0e9751563 debug
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 11:51:06 +11:00
e8abd27f3c initial work on adding LDAP integration
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 11:42:04 +11:00
0619b497f7 new add role feature
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 09:43:59 +11:00
f7168d465a protect unlock api endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-03 17:18:23 +11:00
85bea202f0 endpoint should be POST not GET
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-03 15:12:42 +11:00
e7b2c86ba7 add delete user endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-03 15:05:07 +11:00
f6602f2823 change json key names
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-03 14:50:44 +11:00
35583b5f86 change style
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 11:39:16 +11:00
ec8abc0802 minor update
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 11:15:43 +11:00
396d0c2f00 adjust style
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 11:09:47 +11:00
dfe0c28094 test
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:58:05 +11:00
4d56452f26 tweak css
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:53:05 +11:00
1ace119b02 adjust pandoc
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:47:07 +11:00
ba02b56971 more logging
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:29:35 +11:00
33a5c3dff2 update
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:22:13 +11:00
23af62ef44 retest
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:15:49 +11:00
9983d3e8db test again
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:13:33 +11:00
51c5461632 another fix
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:10:50 +11:00
3801122360 test
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:07:53 +11:00
3e54351477 fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 10:05:11 +11:00
dbcdf56818 test embed directory
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-29 10:03:37 +11:00
3876302bbd test new pandoc command
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 09:56:50 +11:00
fe2fefbe86 test pandoc
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 09:48:22 +11:00
83c126d853 test again
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 09:37:07 +11:00
c00f01b084 more testing
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-29 09:36:20 +11:00
503bcfbd8d more testing
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-29 09:35:50 +11:00
8d2f226090 test pandoc
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-29 09:32:34 +11:00
a7f0bdf09b [ci skip] fix pipeline 2023-12-28 15:54:38 +11:00
fb2dce5414 readme update
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 15:48:21 +11:00
6baa0fe103 tweak readme
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 15:39:40 +11:00
e20e799808 build without cgo
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 15:35:20 +11:00
1e4fd9d5d6 remove markdown commands
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 15:31:36 +11:00
6f47262336 test new build
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-28 15:30:58 +11:00
ca39234f12 remove logging of secure data
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 12:03:48 +11:00
484acd1822 more implementation of runtime unlock
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 11:14:16 +11:00
9203e09d2d test loading secret key
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-27 17:29:02 +11:00
ca316e7086 support querying for secret with username
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-04-19 09:27:06 +10:00
95c6bccefb fix request type for secret list
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-13 14:11:13 +10:00
969d1ca8d0 logic fix
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-05 11:37:26 +10:00
b9a0c3ec0a add list secret api endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-05 11:31:42 +10:00
70f8103901 admin roles should be able to retrieve any secret
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-05 10:42:35 +10:00
1f80d0b9ad default storesecret roleid to roleid of requesting user
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 13:59:28 +10:00
9d15d20a1b [ci skip] readme 2023-04-04 13:30:13 +10:00
05d7af20c1 test some reusable functions to retrieve secrets in different ways
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 13:26:29 +10:00
bf235cdebe bugfix
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 13:04:17 +10:00
747487b764 rename project to smt
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 12:47:50 +10:00
45ceae73c4 fix logging of password on Login
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 12:20:48 +10:00
61f2813802 logging
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 08:56:25 +10:00
93385646b6 don't log hashed passwords
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 08:40:42 +10:00
2d7a55e427 add getUsers
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 08:36:07 +10:00
1c419454a2 update
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 08:31:22 +10:00
ab60f8796a more error handling 2023-04-04 08:31:19 +10:00
43 changed files with 6724 additions and 1052 deletions

View File

@@ -1,7 +1,13 @@
#!/bin/sh
# disable CGO for cross-compiling
export CGO_ENABLED=0
export now=$(TZ=Australia/Sydney date '+%Y%m%d-%H%M%S')
echo $now
#pwd
#ls -lah ~
#go env
echo "build commences"
go build -ldflags "-X main.sha1ver=`git rev-parse HEAD` -X main.buildTime=$now" -o smt
echo "build complete"

View File

@@ -6,23 +6,121 @@ name: default
# Also see https://github.com/harness/drone-cli/blob/master/.drone.yml
steps:
- name: markdown
image: pandoc/core
volumes:
- name: shared
path: /shared
commands:
#- pandoc --standalone --output=index.html --metadata title="SMT Readme" -t html5 README.md
# From https://gitlab.com/vimalkvn/pandoc-mvp-css
- pandoc -s README.md --embed-resources -c www/mvp.css --toc --toc-depth=2 --template template.html -o ./www/index.html
- cp ./www/index.html /shared/index.html
- name: restore-cache-with-filesystem
image: meltwater/drone-cache
pull: true
settings:
backend: "filesystem"
#debug: true
restore: true
cache_key: '{{ .Repo.Name }}_{{ arch }}_{{ os }}'
archive_format: "tar"
filesystem_cache_root: "/go"
local_root: /
mount:
- pkg.mod
- pkg.build
volumes:
- name: cache
path: /go
- name: build
image: golang
environment:
CGO_ENABLED: 0
GOMODCACHE: '/drone/src/pkg.mod'
GOCACHE: '/drone/src/pkg.build'
volumes:
- name: shared
path: /shared
commands:
- cp /shared/index.html ./www/
- sh ./.drone.sh
- name: dell-deploy
# # https://github.com/cschlosser/drone-ftps/blob/master/README.md
image: cschlosser/drone-ftps
environment:
FTP_USERNAME:
from_secret: FTP_USERNAME
FTP_PASSWORD:
from_secret: FTP_PASSWORD
PLUGIN_HOSTNAME: ftp.emc.com:21
PLUGIN_SECURE: false
PLUGIN_VERIFY: false
PLUGIN_CHMOD: false
#PLUGIN_DEBUG: false
PLUGIN_INCLUDE: ^smt$,^smt_checksum.txt$
PLUGIN_EXCLUDE: ^\.git/$,^\uim2/$,^\controllers/$,^\middlewares/$,^\models/$,^\utils/$
# Copy binary to test server
- name: coadcorp-deploy
image: appleboy/drone-scp
settings:
host:
- 10.63.39.130
username: l075239
password:
from_secret: ssh_password
port: 22
command_timeout: 2m
target: /home/l075239/smt/
source:
- smt
- cbs_checksum.txt
- test.env
- www/index.html
# Start service
- name: coadcorp-restart
image: appleboy/drone-ssh
settings:
host:
- 10.63.39.130
username: l075239
password:
from_secret: ssh_password
port: 22
command_timeout: 2m
script:
- sudo bash -c 'mv /home/l075239/smt/test.env /home/l075239/smt/.env'
- sudo bash -c '/etc/init.d/smt restart'
- name: dell-sftp-deploy
image: hypervtechnics/drone-sftp
settings:
host: deft.dell.com
username:
from_secret: DELLFTP_USER
password:
from_secret: DELLFTP_PASS
port: 22
source: ./
filter: smt*
clean: false
target: /
overwrite: true
verbose: true
- name: rebuild-cache-with-filesystem
image: meltwater/drone-cache
pull: true
#when:
# event:
# - tag
settings:
backend: "filesystem"
#debug: true
rebuild: true
cache_key: '{{ .Repo.Name }}_{{ arch }}_{{ os }}'
archive_format: "tar"
filesystem_cache_root: "/go"
mount:
- pkg.mod
- pkg.build
volumes:
- name: cache
path: /go
volumes:
- name: shared
temp: {}
- name: cache
host:
path: /var/lib/cache

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
api\ tests.txt
ccsecrets
ccsecrets.*
smt
smt.*
.env
*.pem
.DS_Store

483
README.md
View File

@@ -1,16 +1,28 @@
# Secrets Management Tool (SMT)
---
title: Secrets Management Tool (SMT)
---
Build Date: `{BUILDTIME}`
Build Hash: `{SHA1VER}`
Go version: `{RUNTIME}`
Written by Nathan Coad (nathan.coad@dell.com)
## Overview
Provide REST API for CRUD to store and retrieve user/password data for logging into devices. Only password is encrypted, via AES256 GCM. Values stored in sqlite database.
Provide REST API to store and retrieve secrets with associated username, device name and optionally device class. Secrets are stored in sqlite database once encrypted using an AES256 block cipher wrapped in Galois Counter Mode with the standard nonce length.
Requires JWT token to store/retrieve passwords.
All secret operations (Create, Read, Update or Delete) or user maangement operations require successful authentication. A JWT token is returned upon login, which must be provided for any other operation.
This isn't super secure, probably not even as secure as Hashicorp Vault running in dev mode.
Users must be a member of a single group. Groups can have access to multiple safes. Groups can have read-only or read-write access to safes. Only users with an admin role can perform user-related operations such as creating users or groups, or creating/deleting safes. Users and groups can be either locally defined or sourced from LDAP lookups.
## Installation
1. Copy binary to chosen location, eg /srv/ccsecrets
Only tested on x64 Linux, but code should compile on other platforms.
1. Copy binary to chosen location, eg /srv/smt/smt
2. Create .env file in same directory as binary, populate as per Configuration section below
3. Create systemd service definition
4. Start service
@@ -18,18 +30,37 @@ This isn't super secure, probably not even as secure as Hashicorp Vault running
## Configuration
|Environment Variable Name| Description | Example | Default |
|--|--|--|--|
| LOG_FILE | Specify the name/path of file to write log messages to | /var/log/ccsecrets.log | ./ccsecrets.log
| LOG_FILE | Specify the name/path of file to write log messages to | /var/log/smt.log | ./smt.log
| BIND_IP | Specify the local IP address to bind to. | 127.0.0.1 | Primary IPv4 address |
| BIND_PORT | Specify the TCP/IP port to bind to. | 443 | 8443 |
| TLS_KEY_FILE | Specify the filename of the TLS certificate private key (must be unencrypted) in PEM format | key.pem | privkey.pem |
| TLS_CERT_FILE | Specify the filename of the TLS certificate file in PEM format | cert.pem | cert.pem |
| TOKEN_HOUR_LIFESPAN | Number of hours that the JWT token returned at login is valid | 12 | No default specified, must define this value |
| API_SECRET | Secret to use when generating JWT token | 3c55990bd479322e2053db3a8 | No default specified, must define this value |
| INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password
| SECRETS_KEY | Key to use for AES256 GCM encryption. Must be exactly 32 bytes | AES256Key-32Characters1234567890 | No default specified, must define this value |
| INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password |
| SECRETS_KEY | Key to use for AES256 GCM encryption. Must be exactly 32 bytes | AES256Key-32Characters1234567890 | No default specified, must define this value or use /api/unlock at runtime |
If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period.
Example for generating API_SECRET and SECRETS_KEY is the following command on linux: `head /dev/urandom | tr -dc A-Za-z0-9 | head -c32`
### LDAP specific configuration
Several optional environment variables are available to configure LDAP integration if required. If these parameters are not specifed, LDAP integration will not be used.
If the LDAP_BIND_ADDRESS is specified, SMT will attempt to perform an LDAP search for the provided username if no matches to the locally configured users are found in the database. This search will utilise the provided credentials to perform the LDAP bind.
This lookup will utilise the sAMAccountName property of the user object in Active Directory. No other LDAP providers have been tested.
Upon successfully verifying the LDAP credentials, SMT will verify if any of the group memberships matches a role defined in the SMT database. If no match is found, the authentication will not succeed.
|Environment Variable Name| Description | Example | Default |
|--|--|--|--|
| LDAP_BIND_ADDRESS | Specify the LDAP Bind address. Only LDAPS on port 636 is supported. Do not specify port 636 in the bind address | dc.example.com | No default specified |
| LDAP_BASE_DN | Specify the base DN to use when binding to AD | "CN=Users,DC=example,DC=com" | No default specified |
| LDAP_TRUST_CERT_FILE | Specify filepath to PEM format public certificate of Certificate Authority signing LDAPS communications | caroot.pem | No default specified, must define this value |
| LDAP_INSECURE_VALIDATION | Specify whether to skip certificate validation when connecting to LDAPS. Do not enable this in production | true | false |
## Systemd script
Create/update the systemd service definition at /etc/systemd/system/smt.service and then run systemctl daemon-reload
@@ -49,84 +80,438 @@ ExecStart=/srv/smt/smt
[Install]
WantedBy=multi-user.target
```
## API
## API Usage
API calls return http status code of **200** if successful, or **4xx** if unsuccessful. API calls that are unsuccessful will also include a JSON response with the key `error` and a value of the reason for the failure. Successful API calls will include a `message` key with a value of either success or something more detailed such as "user deletion success"
API calls that create or modify a record will include the created/updated record in the JSON response.
### Login
**POST** `/api/login`
Body
```
{
"username": "example_username",
"password": "example_password"
}
```
This API call will return a JWT token that must be present for any other API calls to succeed. The validity duration of this token is based on the configured TOKEN_HOUR_LIFESPAN value. JWT token is returned as value of `access_token`, and must be supplied via a HTTP header in the form `"Authorization: Bearer <JWT_TOKEN>"` for all subsequent API calls.
#### Admin Only operations
#### Unlock
**POST** `/api/admin/unlock`
Body
```
{
"secretKey": "Example32ByteSecretKey0123456789"
}
```
If the SECRETS_KEY environment variable is not defined, this API call to unlock stored secrets must be performed after initial startup of SMT. Storing/retrieval of secrets will not succeed until this API call has been made.
This API call can only be made once after the service has started. Subsequent calls will receive an error until the service is restarted.
#### Event Logs
**GET** `/api/admin/logs`
This operation can only be performed by a user with that is admin enabled. Lists all event logs.
### User Operations
#### Register
POST `/api/admin/register`
#### Register User
**POST** `/api/admin/user/add`
Data
Create a new user record by specifying groupId
Body
```
{
"UserName": "",
"Password": "",
"RoleId": 2
"userName": "Test User",
"password": "Example Password",
"groupId": "1",
}
```
This operation can only be performed by a user with a role that is admin enabled. There are 3 built in roles, which can be viewed via the `/api/admin/roles` endpoint.
Create a new user record by specifying groupName
#### Login
POST `/api/login`
Data
Body
```
{
"UserName": "",
"Password": ""
"userName": "Test User",
"password": "Example Password",
"groupName": "Users",
}
```
This API call will return a JWT token that must be present for any other API calls to succeed. The validity duration of this token is based on the configured TOKEN_HOUR_LIFESPAN value. JWT token is returned as value of `access_token`.
Add an ldap user
Body
```
{
"userName": "Ldap User",
"groupName": "Users",
"ldapUser": true
}
```
Registering a user requires specifying the group to which the user will belong. There are 2 built-in groups, with groupName of 'Administrators' or 'Users' and corresponding groupId of 1 and 2 respectively. Available groups can be retrieved via the `/api/admin/groups/list`
This operation can only be performed by a user that is a member of a group with the admin flag enabled, or a user who has the admin flag enabled individually on their database record.
#### Remove User
**POST** `/api/admin/user/delete`
Body
```
{
"userName": "example_username"
}
```
This operation can only be performed by a user that is admin enabled. Removes user account corresponding to specified userName.
#### List Users
**GET** `/api/admin/users`
This operation can only be performed by a user that is admin enabled. Lists currently defined users.
### Permission Operations
Permissions can be assigned either via a group or directly to a user Id. Permissions map the user to the safe(s) they are allowed to access. By default a permission grants read-write access to a safe, although that can be set to read-only if required.
#### List Permissions
**GET** `/api/admin/permissions`
This operation can only be performed by a user that is admin enabled. Lists currently defined permissions.
#### Create Permission
**POST** `/api/admin/permission/add`
Create a new read-only permission directly to a user
Body
```
{
"Description": "Readonly access to default safe",
"safeId": 1,
"userId": 2,
"readOnly": true
}
```
Create a new permission for a group
Body
```
{
"Description": "Group access to default safe",
"safeId": 1,
"groupId": 1
}
```
Creates a new permission mapping user/group to safe. Currently the create permission operation requires knowing the correct user Id or group Id, as well as the safe Id onto which permissions will be granted. This operation can only be performed by a user that is admin enabled.
#### Delete Permission
**POST** `/api/admin/permission/delete`
Delete permission by specifying description
Body
```
{
"Description": "Readonly access to default safe"
}
```
Delete permission by specifying permission id
Body
```
{
"permissionId": 2
}
```
Deletes a permission mapping either a user or a group to a safe. Either the permission description or permission Id can be specified. This operation can only be performed by a user that is admin enabled.
Deleting a permission should be performed prior to deleting any groups specified in that permission.
#### Update Permission
**POST** `/api/admin/permission/update`
Change description of permission
Body
```
{
"permissionId": 2
"Description": "New Permission Description"
}
```
Updates an existing permission. The permissionId must be specified. This operation can only be performed by a user that is admin enabled.
### Group Operations
#### List Groups
**GET** `/api/admin/groups`
This operation can only be performed by a user with that is admin enabled. Lists currently defined groups.
#### Create Group
**POST** `/api/admin/group/add`
Create a new group corresponding with an LDAP group
Body
```
{
"groupName":"ldap access for smt_users",
"ldapGroup":true,
"LdapDn":"CN=smt_users,OU=Groups,DC=example,DC=com"
}
```
Create a new local admin group
Body
```
{
"groupName":"admin group",
"Admin":true,
}
```
Creates a new group, which can be entirely local or mapped to an LDAP security group if LDAP integration is enabled. This operation can only be performed by a user that is admin enabled, or that is a member of a group that is admin enabled.
Ldap group must be specified via the full distinguishedName. The simplest way to get this information is to run the command `dsquery group -name <known group name>` from a windows machine.
#### Delete Group
**POST** `/api/admin/group/delete`
Delete group by specifying group name
Body
```
{
"groupName":"admin group"
}
```
Delete group by specifying group id
Body
```
{
"groupId":2
}
```
Deletes an existing group. Either group name or group Id can be specified. If both are specified, group Id takes precedence. This operation can only be performed by a user that is admin enabled, or that is a member of a group that is admin enabled.
Deleting a group will also impact all permissions based on that group. For that reason, permissions should be removed before a group is deleted.
### Safes Operations
#### List
**GET** `/api/safe/list`
This operation lists all the safes that the currently logged in user has access to.
#### List All
**GET** `/api/admin/safe/listall`
This operation lists all the safes defined in the database. This operation can only be performed by a user that is admin enabled, or that is a member of a group that is admin enabled.
#### Create Safe
**POST** `/api/admin/safe/add`
Create a new safe
Body
```
{
"safeName":"Example second safe"
}
```
This operation creates a new safe in the database. The operation returns details of the created safe, including the allocated safeId. This operation can only be performed by a user that is admin enabled, or that is a member of a group that is admin enabled.
#### Delete Safe
**POST** `/api/admin/safe/delete`
Delete an existing safe
Body
```
{
"safeName":"Example second safe"
}
```
This operation deletes a safe defined in the database. If the safe contained any secrets, they are now inaccessible except to a user that is admin enabled. It is recommended that any secrets stored within the safe are moved to a different safe via the `/api/secret/update` API endpoint.
This operation can only be performed by a user that is admin enabled, or that is a member of a group that is admin enabled.
### Secrets Operations
#### Store
POST `/api/secret/store`
#### Store Secret
**POST** `/api/secret/add`
Data
Store secret if user only has access to a single safe
Body
```
{
"deviceName": "",
"deviceName": "device.example.com",
"deviceCategory": "appliance",
"userName": "example-user",
"secretValue": "example-password"
}
```
Store secret if user only has access to multiple safes
Body
```
{
"safeId": 1,
"deviceName": "device.example.com",
"deviceCategory": "appliance",
"userName": "example-user",
"secretValue": "example-password"
}
```
Must be logged in to execute this command. Permission assigned to logged-in user cannot be a ReadOnly role. Either deviceName or deviceCategory can be blank but not both.
If a secret exists with a matching deviceName and deviceCategory in a safe that the user has access to, then an error will be generated.
If the current user has access to multiple safes, then the destination safeId will also need to be specified.
#### Get Secret
**POST** `/api/secret/get`
Body
```
{
"deviceName": "device.example.com",
"deviceCategory": "",
"userName": "",
"secretValue": ""
"userName": "example-user"
}
```
Must be logged in to execute this command. Role of current user cannot be a ReadOnly role. Secret will be stored with the RoleId of the currently logged in user. Either deviceName or deviceCategory can be blank but not both.
If a secret exists for this RoleId and matching deviceName and deviceCategory then an error will be generated.
#### Retrieve
GET `/api/secret/retrieve`
Data
Body
```
{
"deviceName": "",
"deviceCategory": ""
"secretId": 29
}
```
Must be logged in to execute this command. Only secrets registered with the current user's RoleId can be retrieved.
Must be logged in to execute this command. Only secrets that the logged in user has access to can be retrieved.
Either deviceName or deviceCategory can be specified (or both). Wildcards are supported for both deviceName and deviceCategory fields.
Either deviceName or deviceCategory can be specified (or both). Wildcards are supported for both deviceName and deviceCategory fields. userName can also be specified in conjunction with deviceName or deviceCategory.
1. The percent sign % wildcard matches any sequence of zero or more characters.
2. The underscore _ wildcard matches any single character.
#### Update
POST `/api/secret/update`
If the secretId is known, that can also be used to query for the secret. In this case the secretId uniquely identifies the secret so no other parameters are necessary.
Data
#### Search by device name
**GET** `/api/secret/retrieve/name/<searchname>`
Search for a secret specified by deviceName using a GET request.
Must be logged in to execute this command. Only secrets in safes that the current user can access can be retrieved.
#### Search by device category
**GET** `/api/secret/retrieve/category/<searchname>`
Search for a secret specified by deviceCategory using a GET request.
Must be logged in to execute this command. Only secrets in safes that the current user can access can be retrieved.
#### Search by username
**GET** `/api/secret/retrieve/user/<searchname>`
Search for a secret specified by userName using a GET request.
Must be logged in to execute this command. Only secrets in safes that the current user can access can be retrieved.
#### Update Secret
**POST** `/api/secret/update`
Update secret value for existing secret record
Body
```
{
"deviceName": "",
"deviceCategory": "",
"userName": "",
"secretValue": ""
"deviceName": "device.example.com",
"deviceCategory": "appliance",
"userName": "example-user",
"secretValue": "new-password"
}
```
Users with ReadOnly role will receive Forbidden error when calling this API endpoint. The values specified in deviceName and deviceCategory must match exactly one existing secret record for the RoleId of the currently logged in user. Wildcards are supported for deviceName and deviceCategory.
Move secret into different safe
Body
```
{
"safeId": 2,
"deviceName": "device.example.com",
"deviceCategory": "appliance",
"userName": "example-user",
"secretValue": "example-password"
}
```
The values specified in deviceName and deviceCategory must match exactly one existing secret record for the safes available to the currently logged in user. Wildcards are supported for deviceName and deviceCategory.
If a user has read-write access to multiple safes, then specifying a different safeId to the one currently holding the secret will allow the secret to be moved into the other safe.
#### List Secrets
**GET** `/api/secret/list`
Will generate a list of secrets with their secretId, userName, deviceCategory and deviceName fields, but not secret data. Only secrets belonging to safes that are accessible by the currently logged in user will be returned
#### Delete Secret
**POST** `/api/secret/delete`
Body
```
{
"deviceName": "device.example.com",
"deviceCategory": "",
"userName": "example-user"
}
```
Body
```
{
"secretId": 29
}
```
Deletes specified secret. User must have read-write access to the safe the secret is stored in.
Secret can be specified either by the secretId, or a unique combination of deviceName, deviceCategory and userName.
## Database Schema
![Diagram](www/database.png)

619
api-test.sh Normal file
View File

@@ -0,0 +1,619 @@
#!/bin/bash
set -o pipefail
VERSION='0.3.0'
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
BOLD=$(tput bold)
UNDERLINE=$(tput smul)
RESET=$(tput sgr 0)
ACTION=""
FILE=""
VERBOSE=0
COMMAND_NAME="api-test"
ACCESS_TOKEN=""
ID_TOKEN=""
URL=""
SHOW_HEADER=0
SUPER_SILENT=0
HEADER_ONLY=0
SILENT=0
API_ERROR=0
# Helper methods
echo_v() {
if [ $VERBOSE -eq 1 ]; then
echo $1
fi
}
echo_t() {
printf "\t%s\n" "$1"
}
bytes_to_human() {
b=${1:-0}
d=''
s=0
S=(Bytes {K,M,G,T,E,P,Y,Z}B)
while ((b > 1024)); do
d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))"
b=$((b / 1024))
let s++
done
echo "$b$d ${S[$s]}"
}
color_response() {
case $1 in
2[0-9][0-9]) echo $GREEN ;;
[45][0-9][0-9]) echo $RED ;;
*) ;;
esac
}
# Show usage
function usage() {
case $1 in
run)
echo "Run test cases specified in the test file."
echo ""
echo "USAGE: $COMMAND_NAME [-v] -f file_name run [-hiIs] [ARGS]"
echo ""
echo "OPTIONS:"
echo " -h (--help) print this message"
echo " -i (--include) include header"
echo " -I (--header-only) header only"
echo " -s (--silent) print response status and message only"
echo " -S (--super-silent) print response only"
echo ""
echo "ARGS:"
echo " all Run all test case."
echo " <test_case_name> Run provided test case."
echo ""
echo "EXAMPLE:"
echo "'api-test -f test.json run test_case_1 test_case_2', 'api-test -f test.json run all'"
exit
;;
test)
echo "Run automated tests for a test case."
echo ""
echo "USAGE: $COMMAND_NAME [-v] -f file_name test [ARGS]"
echo ""
echo "OPTIONS:"
echo " -h (--help) print this message"
echo ""
echo "ARGS:"
echo " all Run all automated tests."
echo " <test_case_name> Run provided automated test."
echo ""
echo "EXAMPLE:"
echo "'api-test -f test.json test test_case_1 test_case_2', 'api-test -f test.json test all'"
exit
;;
describe)
echo "List test cases or describe the contents in a test case."
echo ""
echo "USAGE: $COMMAND_NAME [-v] -f file_name describe [ARGS]"
echo ""
echo "OPTIONS:"
echo " -h (--help) print this message"
echo ""
echo "ARGS:"
echo " <empty> List all test case."
echo " <test_case_name> Describe a test case."
echo " <test_case_name> <path> Describe a test case property using json path."
echo ""
echo "EXAMPLE:"
echo "'api-test -f test.json describe', 'api-test -f test.json describe test_case_1', 'api-test -f test.json describe test_case_1 body' "
exit
;;
*)
echo "A simple program to test JSON APIs."
echo ""
echo "USAGE: $COMMAND_NAME [-hv] -f file_name [CMD] [ARGS]"
echo ""
echo "OPTIONS:"
echo " -h (--help) print this message"
echo " -v (--verbose) verbose logging"
echo " -f (--file) file to test"
echo " --version print the version of the program"
echo ""
echo "COMMANDS:"
echo " run Run test cases specified in the test file."
echo " test Run automated test in the test file."
echo " describe List test cases or describe the contents in a test case."
echo ""
echo "Run 'api-test COMMAND --help' for more information on a command."
exit
;;
esac
}
# api methods
call_api() {
ROUTE=$(jq -r ".testCases.\"$1\".path" $FILE)
BODY="$(jq -r ".testCases.\"$1\" | select(.body != null) | .body" $FILE)"
QUERY_PARAMS=$(cat $FILE | jq -r ".testCases.\"$1\" | select(.query != null) | .query | to_entries | map(\"\(.key)=\(.value|tostring)\") | join(\"&\") | \"?\" + . ")
REQUEST_HEADER=$(cat $FILE | jq -r ".testCases.\"$1\" | .header | if . != null then . else {} end | to_entries | map(\"\(.key): \(.value|tostring)\") | join(\"\n\") | if ( . | length) != 0 then \"-H\" + . else \"-H \" end")
METHOD="$(jq -r ".testCases.\"$1\".method //\"GET\" | ascii_upcase" $FILE)"
# curl -ivs --request $METHOD "$URL$ROUTE$QUERY_PARAMS" \
# --data "$BODY" \
# "$COMMON_HEADER" \
# "$REQUEST_HEADER" \
# -w '\n{ "ResponseTime": "%{time_total}s" }\n'
local raw_output=$(curl -is -k --request $METHOD "$URL$ROUTE$QUERY_PARAMS" \
--data "$BODY" \
"$COMMON_HEADER" \
"$REQUEST_HEADER" \
-w '\n{ "ResponseTime": "%{time_total}s", "Size": %{size_download} }' || echo "AUTO_API_ERROR")
if [[ $raw_output == *"AUTO_API_ERROR"* ]]; then
echo "Problem connecting to $URL$ROUTE$QUERY_PARAMS"
#echo "COMMON_HEADER : $COMMON_HEADER"
#echo "REQUEST_HEADER : $REQUEST_HEADER"
#echo $raw_output
API_ERROR=1
return 1
fi
local header="$(awk -v bl=1 'bl{bl=0; h=($0 ~ /HTTP\//)} /^\r?$/{bl=1} {if(h)print $0 }' <<<"$raw_output")"
local json=$(jq -c -R -r '. as $line | try fromjson' <<<"$raw_output")
#echo "JSON body: '${json}'"
RESPONSE_BODY=$(sed -n 1p <<<"$json")
echo "RESPONSE body: '${RESPONSE_BODY}'"
META=$(sed 1d <<<"$json")
META=$(jq -r ".Size = \"$(bytes_to_human $(jq -r '.Size' <<<"$META"))\"" <<<"$META")
parse_header "$header"
}
parse_header() {
local RESPONSE=($(echo "$header" | tr '\r' ' ' | sed -n 1p))
local header=$(echo "$header" | sed '1d;$d' | sed 's/: /" : "/' | sed 's/^/"/' | tr '\r' ' ' | sed 's/ $/",/' | sed '1 s/^/{/' | sed '$ s/,$/}/')
RESPONSE_HEADER=$(echo "$header" "{ \"http_version\": \"${RESPONSE[0]}\",
\"http_status\": \"${RESPONSE[1]}\",
\"http_message\": \"${RESPONSE[@]:2}\",
\"http_response\": \"${RESPONSE[@]:0}\" }" | jq -c -s add)
}
## run specific methods
display_results() {
if [[ $API_ERROR == 1 ]]; then
return
fi
local res=$(jq -r '.http_status + " " + .http_message ' <<<"$RESPONSE_HEADER")
local status=$(jq -r '.http_status' <<<"$RESPONSE_HEADER")
echo "Response:"
echo "${BOLD}$(color_response $status)$res${RESET}"
if [[ $HEADER_ONLY == 1 ]]; then
echo "HEADER:"
echo "$RESPONSE_HEADER" | jq -C '.'
else
if [[ $SHOW_HEADER == 1 ]]; then
echo "HEADER:"
echo "$RESPONSE_HEADER" | jq -C '.'
fi
if [[ $SILENT == 0 ]]; then
echo "BODY:"
echo "$RESPONSE_BODY" | jq -C '.'
fi
fi
if [[ $SUPER_SILENT == 0 ]]; then
echo "META:"
echo "$META" | jq -C '.'
fi
}
api_factory() {
for TEST_CASE in $@; do
API_ERROR=0
echo "${BOLD}Running Case:${RESET} $TEST_CASE"
echo_v "${BOLD}Description: ${RESET}$(jq -r ".testCases.\"$TEST_CASE\".description" $FILE)"
echo_v "${BOLD}Action: ${RESET}$(jq -r ".testCases.\"$TEST_CASE\".method //\"GET\" | ascii_upcase" $FILE) $(jq -r ".testCases.\"$TEST_CASE\".path" $FILE)"
call_api $TEST_CASE
display_results
echo ""
echo ""
done
}
test_factory() {
TOTAL_TEST_CASE=0
TOTAL_FAIL_CASE=0
ANY_API_ERROR=0
for TEST_CASE in $@; do
API_ERROR=0
echo "${BOLD}Testing Case:${RESET} $TEST_CASE"
echo_v "${BOLD}Description: ${RESET}$(jq -r ".testCases.\"$TEST_CASE\".description" $FILE)"
echo_v "${BOLD}Action: ${RESET}$(jq -r ".testCases.\"$TEST_CASE\".method //\"GET\" | ascii_upcase" $FILE) $(jq -r ".testCases.\"$TEST_CASE\".path" $FILE)"
if [[ -z $(jq -r ".testCases.\"$TEST_CASE\".expect? | select(. !=null)" $FILE) ]]; then
tput cuf 2
echo "No test cases found"
echo ""
echo ""
continue
fi
call_api $TEST_CASE
if [[ $API_ERROR == 1 ]]; then
ANY_API_ERROR=1
tput cuf 2
echo -e "${BOLD}${RED}Error running tests after failed api request for '$TEST_CASE' ${RESET}"
echo -e "\n"
continue
fi
local TEST_SCENARIO=$(jq -r ".testCases.\"$TEST_CASE\".expect.header? | select(. !=null and . != {})" $FILE)
if [[ ! -z $TEST_SCENARIO ]]; then
tput cuf 2
echo "${UNDERLINE}Checking condition for header${RESET}"
test_runner $TEST_CASE "header" "$RESPONSE_HEADER"
echo ""
echo ""
fi
TEST_SCENARIO=$(jq -r ".testCases.\"$TEST_CASE\".expect.body? | select(. !=null and . != {})" $FILE)
if [[ ! -z $TEST_SCENARIO ]]; then
tput cuf 2
echo "${UNDERLINE}Checking condition for body${RESET}"
echo "$RESPONSE_BODY"
test_runner $TEST_CASE "body" "$RESPONSE_BODY"
echo ""
echo ""
fi
TEST_SCENARIO=$(jq -r ".testCases.\"$TEST_CASE\".expect.external? | select(. !=null and . != \"\")" $FILE)
if [[ ! -z $TEST_SCENARIO ]]; then
tput cuf 2
echo "${UNDERLINE}Checking condition from external program${RESET}"
external_script "$TEST_SCENARIO" "$TEST_CASE" "$RESPONSE_BODY" "$RESPONSE_HEADER"
TOTAL_TEST_CASE=$((TOTAL_TEST_CASE + 1))
echo ""
echo ""
fi
done
echo -e "${BOLD}Total tests:\t$TOTAL_TEST_CASE"
if [[ $(($TOTAL_TEST_CASE - $TOTAL_FAIL_CASE)) != 0 ]]; then
printf $GREEN
fi
echo -e "${BOLD}Total success:\t$(($TOTAL_TEST_CASE - $TOTAL_FAIL_CASE))${RESET}"
if [[ $TOTAL_FAIL_CASE != 0 ]]; then
printf $RED
else
if [[ $ANY_API_ERROR != 0 ]]; then
echo -e "\n${BOLD}${RED}Some test cases failed to connect to the requested api.${RESET}"
exit 1
else
echo -e "\n${BOLD}${GREEN}All tests ran successfully!${RESET}"
fi
exit 0
fi
echo -e "${BOLD}Total failure:\t$TOTAL_FAIL_CASE${RESET}"
echo -e "\n${BOLD}${RED}Tests Failed!${RESET}"
exit 1
}
test_runner() {
for test in ""contains eq path_eq path_contains hasKeys[]""; do
local TEST_SCENARIO=$(jq -c -r ".testCases.\"$1\".expect.$2.$test? | select(. !=null)" $FILE)
if [[ -z $TEST_SCENARIO ]]; then
continue
fi
TOTAL_TEST_CASE=$((TOTAL_TEST_CASE + 1))
tput cuf 4
if [[ $test == "contains" ]]; then
echo "Checking contains comparision${RESET}"
contains "$TEST_SCENARIO" "$3"
elif [[ $test == "eq" ]]; then
echo "Checking equality comparision${RESET}"
check_eq "$TEST_SCENARIO" "$3"
elif [[ $test == "path_eq" ]]; then
echo "Checking path equality comparision${RESET}"
path_checker "$TEST_SCENARIO" "$3"
elif [[ $test == "path_contains" ]]; then
echo "Checking path contains comparision${RESET}"
path_checker "$TEST_SCENARIO" "$3" 1
else
echo "Checking has key comparision${RESET}"
has_key "$TEST_SCENARIO" "$3"
fi
done
}
external_script() {
$1 "$2" "$3" "$4"
local EXIT_CODE=$?
if [[ $EXIT_CODE == 0 ]]; then
tput cuf 4
echo "${GREEN}${BOLD}Check Passed${RESET}"
else
tput cuf 4
echo "${RED}${BOLD}Check Failed${RESET}"
TOTAL_FAIL_CASE=$((TOTAL_FAIL_CASE + 1))
fi
}
contains() {
tput cuf 6
local check=$(jq -c --argjson a "$1" --argjson b "$2" -n '$a | select(. != null) | $b | contains($a)')
if [[ $check == "true" ]]; then
echo "${GREEN}${BOLD}Check Passed${RESET}"
else
echo "${RED}${BOLD}Check Failed${RESET}"
TOTAL_FAIL_CASE=$((TOTAL_FAIL_CASE + 1))
echo "EXPECTED:"
echo "${GREEN}$1${RESET}"
echo "GOT:"
echo "${RED}$2${RESET}"
echo ""
fi
}
has_key() {
# local paths=$(jq -r 'def path2text($value):
# def tos: if type == "number" then . else "\"\(tojson)\"" end;
# reduce .[] as $segment (""; .
# + ($segment
# | if type == "string" then "." + . else "[\(.)]" end));
# paths(scalars) as $p
# | getpath($p) as $v
# | $p | path2text($v)' <<<"$2")
local paths=$(jq -r 'path(..)|[.[]|tostring]|join(".")' <<<"$2")
tput cuf 6
for path in $1; do
local FOUND=0
for data_path in $paths; do
if [[ "$path" == "$data_path" ]]; then
FOUND=1
break
fi
done
if [[ $FOUND == 0 ]]; then
echo "${RED}${BOLD}Check Failed${RESET}"
TOTAL_FAIL_CASE=$((TOTAL_FAIL_CASE + 1))
echo "CANNOT FIND KEY:"
echo "${RED}$path${RESET}"
echo ""
return
fi
done
echo "${GREEN}${BOLD}Check Passed${RESET}"
}
check_eq() {
tput cuf 6
local type=$(jq -r -c --argjson a "$1" -n '$a|type' 2>/dev/null)
local check
if [[ $type == "object" || $type == "array" ]]; then
check=$(jq -c --argjson a "$1" --argjson b "$2" -n 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); ($a | (post_recurse | arrays) |= sort) as $a | ($b | (post_recurse | arrays) |= sort) as $b | $a == $b')
elif [[ $type == "number" || $type == "boolean" || $type == "null" || $type == "string" ]]; then
check=$(jq -c --argjson a "$1" --argjson b "$2" -n '$a == $b')
else
if [[ $1 == $2 ]]; then
check="true"
else
check="false"
fi
fi
if [[ $check == "true" ]]; then
echo "${GREEN}${BOLD}Check Passed${RESET}"
else
tput cuf 2
echo "${RED}${BOLD}Check Failed${RESET}"
TOTAL_FAIL_CASE=$((TOTAL_FAIL_CASE + 1))
echo "EXPECTED:"
echo "${GREEN}$1${RESET}"
echo "GOT:"
echo "${RED}$2${RESET}"
echo ""
fi
}
path_checker() {
local keys=$(jq -c -r --argjson a "$1" -n '$a | keys[]')
if [[ -z "$keys" ]]; then
return
fi
for key in $keys; do
tput cuf 6
local value=$(jq -c -r --argjson a "$1" -n "\$a | .\"$key\"")
echo "When path is '$key'"
local compare_value=$(jq -c -r --argjson a "$2" -n "\$a | try .$key catch \"OBJECT_FETCH_ERROR_JQ_API_TEST\"" 2>/dev/null)
if [[ -z "$compare_value" ]]; then
tput cuf 8
echo "${RED}${BOLD}Check Failed${RESET}"
TOTAL_FAIL_CASE=$((TOTAL_FAIL_CASE + 1))
tput cuf 2
echo "INVALID PATH SYNTAX: ${RED}data[0]target_id${RESET}"
return
fi
tput cuf 2
if [[ $3 == 1 ]]; then
contains "$value" "$compare_value"
else
check_eq "$value" "$compare_value"
fi
done
}
# run command
run() {
for arg in "$@"; do
case $arg in
-i | --include)
SHOW_HEADER=1
shift
;;
-I | --header-only)
HEADER_ONLY=1
shift
;;
-s | --silent)
SILENT=1
shift
;;
-S | --super-silent)
SILENT=1
SUPER_SILENT=1
shift
;;
-h | --help)
usage run
exit
;;
esac
done
case $1 in
all)
api_factory "$(jq -r '.testCases | keys[]' $FILE)"
;;
'') usage run ;;
*)
api_factory $@
;;
esac
}
# test command
test() {
for arg in "$@"; do
case $arg in
-h | --help)
usage test
exit
;;
esac
done
case $1 in
all)
test_factory "$(jq -r '.testCases | keys[]' $FILE)"
;;
'')
usage test
;;
*)
test_factory $@
;;
esac
}
# describe command
describe() {
for arg in "$@"; do
case $arg in
-h | --help)
usage describe
exit
;;
esac
done
case $1 in
'')
echo -e "S.N.\tTest case"
jq -r '.testCases | keys[]' $FILE | awk '{print NR "\t" $0}'
;;
*)
jq -r ".testCases | .$1 | .$2?" $FILE
;;
esac
}
# INIT COMMANDS AND CHECKS
for arg in "$@"; do
case $arg in
run | test | describe)
ACTION="$1"
shift
break
;;
-f | --file)
FILE="$2"
shift
;;
-h | --help)
usage
exit
;;
--version)
echo "api-test version $VERSION"
exit
;;
-v | --verbose)
VERBOSE=1
shift
;;
*)
shift
;;
esac
done
# Check for dependency programs
command -v curl >/dev/null 2>&1 || {
echo >&2 "This program requires 'curl' to run. Please install 'curl'"
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo >&2 "This program requires 'jq' to run. Please install 'jq'"
exit 1
}
if [ ! -f "$FILE" ]; then
DEFAULT_FILE=("test.json api-test.json template.json")
FOUND_FILE=0
for default in $DEFAULT_FILE; do
if [ -f "$default" ]; then
FOUND_FILE=1
FILE=$default
break
fi
done
if [[ $FOUND_FILE == 0 ]]; then
echo "Please provide an existing file."
exit 1
fi
fi
jq empty $FILE
if [ $? -ne 0 ]; then
exit 1
fi
# Check if url is present
URL=$(jq -r '.url | select( . != null)' $FILE)
if [[ -z $URL ]]; then
echo "'url' is a required field in base object of a test file and must be a string."
exit 1
fi
COMMON_HEADER=$(cat $FILE | jq -r -c ". | .header | if . != null then . else {} end | to_entries | map(\"\(.key): \(.value|tostring)\") | join(\"\n\") | if ( . | length) != 0 then \"-H\" + . else \"-H \" end")
# Check if test cases is present
if [[ -z $(jq -r '.testCases | select(. != null and . != {})' $FILE) ]]; then
echo "'testCases' is a required field in base object of a test file and must have atleast one test case."
exit 1
fi
case $ACTION in
run)
run $@
;;
test) test $@ ;;
describe) describe $@ ;;
*)
usage
;;
esac

View File

@@ -1,64 +1,175 @@
package controllers
import (
"errors"
"fmt"
"html"
"log"
"net/http"
"strings"
"ccsecrets/models"
"ccsecrets/utils/token"
"smt/models"
"smt/utils/token"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
RoleId int `json:"roleid"`
type AddUserInput struct {
UserName string `json:"userName" binding:"required"`
Password string `json:"password"`
GroupId int `json:"groupId"`
GroupName string `json:"groupName"`
LdapUser bool `json:"ldapUser"`
//RoleId int `json:"roleid"`
}
type LoginInput struct {
Username string `json:"username" binding:"required"`
UserName string `json:"userName" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var input RegisterInput
type DeleteInput struct {
UserName string `json:"userName" binding:"required"`
}
type AddRoleInput struct {
RoleName string `json:"roleName" binding:"required"`
LdapGroup string `json:"ldapGroup"`
ReadOnly bool `json:"readOnly"`
Admin bool `json:"admin"`
}
func DeleteUser(c *gin.Context) {
var input DeleteInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{}
//u.RoleId = 1
u.UserName = input.Username
u.Password = input.Password
// Default to regular user role if not specified
if input.RoleId == 0 {
log.Printf("Register no role specified, defaulting to RoleId of 2.\n")
u.RoleId = 2
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
u.RoleId = input.RoleId
RequestingUserId = val.(int)
}
u := models.User{}
u.UserName = input.UserName
//remove spaces in username
u.UserName = html.EscapeString(strings.TrimSpace(u.UserName))
// Confirm user account exists
testUser, _ := models.UserGetByName(u.UserName)
log.Printf("DeleteUser confirming user '%s' account exists\n", u.UserName)
if (models.User{} == testUser) {
err := errors.New("attempt to delete non-existing username '" + u.UserName + "'")
log.Printf("Delete User error : '%s'\n", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
} else {
err := u.DeleteUser()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"Error deleting user": err.Error()})
return
}
// Create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Deleted User Id %d", testUser.UserId),
}
a.AuditLogAdd()
c.JSON(http.StatusOK, gin.H{"message": "user deletion success"})
}
}
func AddUser(c *gin.Context) {
var input AddUserInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(input.UserName) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "username must be specified"})
return
}
if len(input.Password) == 0 && !input.LdapUser {
c.JSON(http.StatusBadRequest, gin.H{"error": "password must be specified for non-ldap user"})
return
}
if input.LdapUser && len(input.Password) > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "password should not be specified for ldap user"})
return
}
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
RequestingUserId = val.(int)
}
u := models.User{}
u.UserName = input.UserName
u.Password = input.Password
u.LdapUser = input.LdapUser
// Determine which GroupId to save
// Can be specified either by GroupName or GroupId in the request
if len(input.GroupName) > 0 {
g, err := models.GroupGetByName(input.GroupName)
if err != nil {
errString := fmt.Sprintf("RegisterUser error looking up group by name : '%s'", err)
log.Println(errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if g == (models.Group{}) {
errString := fmt.Sprintf("RegisterUser specified group '%s' not found\n", input.GroupName)
log.Println(errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
} else {
u.GroupId = g.GroupId
}
} else if input.GroupId > 0 {
u.GroupId = input.GroupId
} else {
// Default to group Id 2 which is users
log.Println("RegisterUser no group id or group name specified, defaulting to built-in group 'Users' ")
u.GroupId = 2
}
//remove spaces in username
u.UserName = html.EscapeString(strings.TrimSpace(u.UserName))
// Check if user already exists
testUser, _ := models.GetUserByName(u.UserName)
log.Printf("Register checking if user already exists : '%v'\n", testUser)
testUser, _ := models.UserGetByName(u.UserName)
log.Printf("Register checking if user '%s' already exists\n", u.UserName)
if (models.User{} == testUser) {
log.Printf("Register confirmed no existing username\n")
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Attempt to register conflicting username"})
err := errors.New("attempt to register conflicting username '" + u.UserName + "'")
log.Printf("Register error : '%s'\n", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
//turn password into hash
//turn password into hash if defined
if len(input.Password) > 0 {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"Error hashing password": err.Error()})
@@ -68,15 +179,24 @@ func Register(c *gin.Context) {
log.Printf("Register generated hashed password value '%s'\n", string(hashedPassword))
}
u.Password = string(hashedPassword)
}
_, err = u.SaveUser()
_, err := u.SaveUser()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"Error saving user": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "registration success"})
// Create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Created User '%s' with id %d", u.UserName, u.UserId),
}
a.AuditLogAdd()
c.JSON(http.StatusOK, gin.H{"message": "user registration success", "data": u})
}
func Login(c *gin.Context) {
@@ -90,16 +210,19 @@ func Login(c *gin.Context) {
u := models.User{}
u.UserName = input.Username
u.UserName = input.UserName
u.Password = input.Password
log.Printf("Login checking username '%s' and password '%s'\n", u.UserName, u.Password)
log.Printf("Login checking username '%s' and password length '%d'\n", u.UserName, len(u.Password))
token, err := models.LoginCheck(u.UserName, u.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is incorrect."})
c.JSON(http.StatusUnauthorized, gin.H{"error": "username or password is incorrect."})
return
} else {
//log.Printf("Login verified, returning token '%s'\n", token)
log.Printf("Login verified, returning token\n")
}
c.JSON(http.StatusOK, gin.H{"access_token": token})
@@ -115,7 +238,7 @@ func CurrentUser(c *gin.Context) {
return
}
u, err := models.GetUserByID(user_id)
u, err := models.UserGetByID(user_id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -125,14 +248,14 @@ func CurrentUser(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "success", "data": u})
}
func GetRoles(c *gin.Context) {
roles, err := models.QueryRoles()
func GetUsers(c *gin.Context) {
users, err := models.UserList()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success", "data": roles})
c.JSON(http.StatusOK, gin.H{"message": "success", "data": users})
}

View File

@@ -0,0 +1,196 @@
package controllers
import (
"fmt"
"html"
"log"
"net/http"
"smt/models"
"strings"
"github.com/gin-gonic/gin"
)
type GroupInput struct {
GroupId int `db:"GroupId" json:"groupId"`
GroupName string `db:"GroupName" json:"groupName"`
LdapGroup bool `db:"LdapGroup" json:"ldapGroup"`
LdapDn string `db:"LdapDn" json:"ldapDn"`
Admin bool `db:"Admin" json:"admin"`
}
func GetGroupsHandler(c *gin.Context) {
groups, err := models.GroupList()
if err != nil {
errString := fmt.Sprintf("error retrieving groups : '%s'", err)
log.Printf("GetGroups %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success", "data": groups})
}
func AddGroupHandler(c *gin.Context) {
var input GroupInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(input.GroupName) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no group name specified"})
return
}
if input.LdapGroup && len(input.LdapDn) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ldapGroup is true but no ldapDn specified"})
return
}
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
RequestingUserId = val.(int)
}
g := models.Group{}
g.GroupName = input.GroupName
g.LdapGroup = input.LdapGroup
g.LdapDn = input.LdapDn
g.Admin = input.Admin
//remove leading/trailing spaces in groupname
g.GroupName = html.EscapeString(strings.TrimSpace(g.GroupName))
// Check if there is already an LDAP group with the same Dn
if g.LdapGroup {
// TODO check for existing LDAP group
testLdapGroup, _ := models.GroupGetByLdapDn(g.LdapDn)
if (models.Group{} == testLdapGroup) {
log.Printf("AddGroupHandler confirmed no existing group for same LDAP DN\n")
} else {
errorString := fmt.Sprintf("attempt to register group with same ldap DN as existing group '%s'", g.GroupName)
log.Printf("AddGroupHandler error : '%s'\n", errorString)
c.JSON(http.StatusBadRequest, gin.H{"error": errorString})
return
}
}
// Check if group already exists with same name
testGroup, _ := models.GroupGetByName(g.GroupName)
log.Printf("AddGroupHandler checking if group '%s' already exists\n", g.GroupName)
if (models.Group{} == testGroup) {
log.Printf("AddGroupHandler confirmed no existing group name\n")
} else {
errorString := fmt.Sprintf("attempt to register conflicting groupname '%s'", g.GroupName)
log.Printf("AddGroupHandler error : '%s'\n", errorString)
c.JSON(http.StatusBadRequest, gin.H{"error": errorString})
return
}
// Verification checks passed, return group
group, err := g.GroupAdd()
// Create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Created Group '%s' with id %d", g.GroupName, g.GroupId),
}
a.AuditLogAdd()
if err != nil {
errString := fmt.Sprintf("error creating group : '%s'", err)
log.Printf("AddGroupHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "group creation success", "data": group})
}
func DeleteGroupHandler(c *gin.Context) {
var input GroupInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Input validation
if input.GroupId == 0 && len(input.GroupName) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no group name or id specified"})
return
}
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
RequestingUserId = val.(int)
}
g := models.Group{}
g.GroupId = input.GroupId
g.GroupName = input.GroupName
if g.GroupId > 0 { // Group Id was specified
// Confirm group exists
testGroup, _ := models.GroupGetById(g.GroupId)
log.Printf("DeleteGroupHandler confirming group id '%d' exists\n", g.GroupId)
if (models.Group{} == testGroup) {
errString := fmt.Sprintf("attempt to delete non-existing group id '%d'", g.GroupId)
log.Printf("DeleteGroupHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
g.GroupName = testGroup.GroupName
} else if len(g.GroupName) > 0 { // Group name was specified
//remove leading/trailing spaces in groupname
g.GroupName = html.EscapeString(strings.TrimSpace(g.GroupName))
// Confirm group exists
testGroup, _ := models.GroupGetByName(g.GroupName)
log.Printf("DeleteGroupHandler confirming group '%s' exists\n", g.GroupName)
if (models.Group{} == testGroup) {
errString := fmt.Sprintf("attempt to delete non-existing group '%s'", g.GroupName)
log.Printf("DeleteGroupHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
g.GroupId = testGroup.GroupId
}
// TODO verify no permissions refer to this group still
err := g.GroupDelete()
// Create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Deleted Group '%s' with id %d", g.GroupName, g.GroupId),
}
a.AuditLogAdd()
if err != nil {
errString := fmt.Sprintf("error deleting group : '%s'", err)
log.Printf("DeleteGroupHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "group deletion success"})
}

View File

@@ -0,0 +1,267 @@
package controllers
import (
"fmt"
"html"
"log"
"net/http"
"smt/models"
"smt/utils"
"strings"
"github.com/gin-gonic/gin"
)
type PermissionInput struct {
PermissionId int `db:"PermissionId" json:"permissionId"`
Description string `db:"Description" json:"description"`
ReadOnly bool `db:"ReadOnly" json:"readOnly"`
SafeId int `db:"SafeId" json:"safeId"`
UserId int `db:"UserId" json:"userId"`
GroupId int `db:"GroupId" json:"groupId"`
}
func GetPermissionsHandler(c *gin.Context) {
permissions, err := models.PermissionList()
if err != nil {
errString := fmt.Sprintf("error retrieving permissions : '%s'", err)
log.Printf("GetPermissionsHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success", "data": permissions})
}
func AddPermissionHandler(c *gin.Context) {
var input PermissionInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate input
if len(input.Description) == 0 && input.PermissionId == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no permission id or description specified"})
return
}
if input.SafeId == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no safe id specified"})
return
}
if input.UserId == 0 && input.GroupId == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no user id or group id specified"})
return
}
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
RequestingUserId = val.(int)
}
p := models.Permission{
PermissionId: input.PermissionId,
Description: input.Description,
ReadOnly: input.ReadOnly,
SafeId: input.SafeId,
UserId: input.UserId,
GroupId: input.GroupId,
}
//remove leading/trailing spaces in permission description
p.Description = html.EscapeString(strings.TrimSpace(p.Description))
// Check if permission definition already exists
testPermission, _ := models.PermissionGetByDesc(p.Description)
log.Printf("AddPermissionHandler checking if permissions with description '%s' already exists\n", p.Description)
if (models.Permission{} == testPermission) {
log.Printf("AddPermissionHandler confirmed no permission with same description\n")
} else {
errorString := fmt.Sprintf("attempt to register permissions with description '%s' but id '%d' already exists", p.Description, testPermission.PermissionId)
log.Printf("Register error : '%s'\n", errorString)
c.JSON(http.StatusBadRequest, gin.H{"error": errorString})
return
}
_, err := p.PermissionAdd()
// Create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Created Permission '%s' with id %d on safe id %d for group id %d or user id %d", p.Description, p.PermissionId, p.SafeId, p.GroupId, p.UserId),
}
a.AuditLogAdd()
if err != nil {
errString := fmt.Sprintf("error creating permission : '%s'", err)
log.Printf("AddPermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "permission creation success", "data": p})
}
func DeletePermissionHandler(c *gin.Context) {
var input PermissionInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Input validation
if input.PermissionId == 0 && len(input.Description) == 0 {
errString := "no permission description or id specified"
log.Printf("DeletePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
RequestingUserId = val.(int)
}
p := models.Permission{
PermissionId: input.PermissionId,
Description: input.Description,
ReadOnly: input.ReadOnly,
SafeId: input.SafeId,
UserId: input.UserId,
GroupId: input.GroupId,
}
//remove leading/trailing spaces in permission description
p.Description = html.EscapeString(strings.TrimSpace(p.Description))
// Check if permission definition already exists
if len(p.Description) > 0 {
log.Printf("DeletePermissionHandler confirming permission with description '%s' exists\n", p.Description)
testPermission, _ := models.PermissionGetByDesc(p.Description)
if (models.Permission{} == testPermission) {
errString := fmt.Sprintf("attempt to delete non-existing permission with description '%s'", p.Description)
log.Printf("DeletePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
} else {
log.Printf("DeletePermissionHandler confirming permission with id '%d' exists\n", p.PermissionId)
testPermission, _ := models.PermissionGetById(p.PermissionId)
if (models.Permission{} == testPermission) {
errString := fmt.Sprintf("attempt to delete non-existing permission with id '%d'", p.PermissionId)
log.Printf("DeletePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
}
err := p.PermissionDelete()
// Create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Deleted Permission '%s' with id %d", p.Description, p.PermissionId),
}
a.AuditLogAdd()
if err != nil {
errString := fmt.Sprintf("error deleting permission : '%s'", err)
log.Printf("DeletePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "permission deletion success"})
}
func UpdatePermissionHandler(c *gin.Context) {
var input PermissionInput
var RequestingUserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Input validation
if input.PermissionId == 0 {
errString := "must specify permission id"
log.Printf("UpdatePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
RequestingUserId = val.(int)
}
// Check specified permission currently exists
currentPermission, err := models.PermissionGetById(input.PermissionId)
if err != nil {
errString := fmt.Sprintf("error querying existing permission : '%s'", err)
log.Printf("UpdatePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if (models.Permission{} == currentPermission) {
errString := fmt.Sprintf("no permission id '%d' found", input.PermissionId)
log.Printf("UpdatePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// create new struct with values supplied by user
newPermission := models.Permission{
PermissionId: input.PermissionId,
Description: input.Description,
ReadOnly: input.ReadOnly,
SafeId: input.SafeId,
UserId: input.UserId,
GroupId: input.GroupId,
}
//remove leading/trailing spaces in permission description
newPermission.Description = html.EscapeString(strings.TrimSpace(newPermission.Description))
// Copy newPermission into currentPermission
utils.UpdateStruct(&currentPermission, &newPermission)
// run the database update
_, err = currentPermission.PermissionUpdate()
if err != nil {
errString := fmt.Sprintf("error updating permission : '%s'", err)
log.Printf("UpdatePermissionHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
//create audit record
a := models.Audit{
UserId: RequestingUserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Updated Permission '%s' with id %d", currentPermission.Description, currentPermission.PermissionId),
}
a.AuditLogAdd()
c.JSON(http.StatusOK, gin.H{"message": "permission update success", "data": currentPermission})
}

137
controllers/controlSafes.go Normal file
View File

@@ -0,0 +1,137 @@
package controllers
import (
"fmt"
"html"
"log"
"net/http"
"smt/models"
"strings"
"github.com/gin-gonic/gin"
)
type SafeInput struct {
SafeId int `db:"SafeId" json:"safeId"`
SafeName string `db:"SafeName" json:"safeName"`
}
// GetSafesHandler provides a list of all safes that a user has access to
func GetSafesHandler(c *gin.Context) {
var UserId int
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
UserId = val.(int)
}
safes, err := models.SafeListAllowed(UserId)
if err != nil {
errString := fmt.Sprintf("error retrieving safes : '%s'", err)
log.Printf("GetSafesHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success", "data": safes})
}
// GetAllSafesHandler provides an admin user a list of all safes that exist in the database
func GetAllSafesHandler(c *gin.Context) {
safes, err := models.SafeList()
if err != nil {
errString := fmt.Sprintf("error retrieving safes : '%s'", err)
log.Printf("GetSafesHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success", "data": safes})
}
func AddSafeHandler(c *gin.Context) {
var input SafeInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(input.SafeName) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no safe name specified"})
return
}
s := models.Safe{SafeId: input.SafeId, SafeName: input.SafeName}
//remove leading/trailing spaces in safe name
s.SafeName = html.EscapeString(strings.TrimSpace(s.SafeName))
// Check if safe already exists
testSafe, _ := models.SafeGetByName(s.SafeName)
log.Printf("AddSafeHandler checking if safe '%s' already exists\n", s.SafeName)
if (models.Safe{} == testSafe) {
log.Printf("AddSafeHandler confirmed no existing safe name\n")
} else {
errorString := fmt.Sprintf("attempt to register conflicting safe '%s'", s.SafeName)
log.Printf("Register error : '%s'\n", errorString)
c.JSON(http.StatusBadRequest, gin.H{"error": errorString})
return
}
_, err := s.SafeAdd()
if err != nil {
errString := fmt.Sprintf("error creating safe : '%s'", err)
log.Printf("AddSafeHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "safe creation success", "data": s})
}
func DeleteSafeHandler(c *gin.Context) {
var input SafeInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Input validation
if input.SafeId == 0 && len(input.SafeName) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no safe name or id specified"})
return
}
s := models.Safe{SafeId: input.SafeId, SafeName: input.SafeName}
//remove leading/trailing spaces in safe name
s.SafeName = html.EscapeString(strings.TrimSpace(s.SafeName))
// Confirm safe exists
testSafe, _ := models.SafeGetByName(s.SafeName)
log.Printf("DeleteSafeHandler confirming group '%s' exists\n", s.SafeName)
if (models.Safe{} == testSafe) {
errString := fmt.Sprintf("attempt to delete non-existing safe '%s'", s.SafeName)
log.Printf("DeleteSafeHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
} else {
err := s.SafeDelete()
if err != nil {
errString := fmt.Sprintf("error deleting safe : '%s'", err)
log.Printf("DeleteSafeHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "safe deletion success"})
}
}

View File

@@ -0,0 +1,23 @@
package controllers
import (
"fmt"
"log"
"net/http"
"smt/models"
"github.com/gin-gonic/gin"
)
func GetAuditLogsHandler(c *gin.Context) {
logs, err := models.AuditLogList()
if err != nil {
errString := fmt.Sprintf("error retrieving audit logs : '%s'", err)
log.Printf("GetAuditLogsHandler %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success", "data": logs})
}

View File

@@ -0,0 +1,219 @@
package controllers
import (
"fmt"
"log"
"net/http"
"smt/models"
"time"
"github.com/gin-gonic/gin"
)
type RetrieveInput struct {
SecretId int `json:"secretId"`
DeviceName string `json:"deviceName"`
DeviceCategory string `json:"deviceCategory"`
UserName string `json:"userName"`
SafeId int `json:"safeId"`
SafeName string `json:"safeName"`
}
type ListSecret struct {
SecretId int `db:"SecretId" json:"secretId"`
SafeId int `db:"SafeId" json:"safeId"`
DeviceName string `db:"DeviceName" json:"deviceName"`
DeviceCategory string `db:"DeviceCategory" json:"deviceCategory"`
UserName string `db:"UserName" json:"userName"`
Secret string `db:"Secret" json:"-"`
LastUpdated time.Time `db:"LastUpdated" json:"lastUpdated"`
}
func RetrieveSecret(c *gin.Context) {
var input RetrieveInput
// Validate the input matches our struct
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
//log.Printf("RetrieveSecret received JSON input '%v'\n", input)
// Populate fields
s := models.Secret{}
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
s.UserName = input.UserName
if input.SecretId > 0 {
s.SecretId = input.SecretId
}
if input.DeviceName == "" && input.DeviceCategory == "" && input.UserName == "" && input.SecretId == 0 {
errString := "no values provided to select secret"
log.Printf("RetrieveSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
retrieveSpecifiedSecret(&s, c)
}
func RetrieveSecretByDevicename(c *gin.Context) {
DeviceName := c.Param("devicename")
if DeviceName == "" {
errString := "no devicename value specified"
log.Printf("RetrieveSecretByDevicename %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// Create object based on specified data
s := models.Secret{DeviceName: DeviceName}
//s.DeviceName = DeviceName
retrieveSpecifiedSecret(&s, c)
}
func RetrieveSecretByDevicecategory(c *gin.Context) {
DeviceCategory := c.Param("devicecategory")
if DeviceCategory == "" {
errString := "no devicecategory value specified"
log.Printf("RetrieveSecretByDevicecategory %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// Create object based on specified data
s := models.Secret{DeviceCategory: DeviceCategory}
retrieveSpecifiedSecret(&s, c)
}
func RetrieveSecretByUsername(c *gin.Context) {
userName := c.Param("username")
if userName == "" {
errString := "no username value specified"
log.Printf("RetrieveSecretByUsername %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// Create object based on specified data
s := models.Secret{UserName: userName}
retrieveSpecifiedSecret(&s, c)
}
func retrieveSpecifiedSecret(s *models.Secret, c *gin.Context) {
var UserId int
var results []models.Secret
// Get userId that we stored in the context earlier
if val, ok := c.Get("user-id"); !ok {
errString := "error determining user"
log.Printf("retrieveSpecifiedSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
} else {
UserId = val.(int)
}
// Work out which safe to query for this user if the safe was not specified
safeList, err := models.UserGetSafesAllowed(int(UserId))
if err != nil {
errString := "error determining user safes"
log.Printf("retrieveSpecifiedSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// If there was only one result then just use that
if len(safeList) == 0 {
errString := "no matching secret or user has no access to specified secret"
log.Printf("retrieveSpecifiedSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
} else if len(safeList) == 1 {
s.SafeId = safeList[0].SafeId
results, err = models.SecretsGetFromMultipleSafes(s, []int{s.SafeId})
} else {
// Create a list of all the safes this user can access
var safeIds []int
for _, safe := range safeList {
safeIds = append(safeIds, safe.SafeId)
}
results, err = models.SecretsGetFromMultipleSafes(s, safeIds)
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(results) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "found no matching secrets"})
return
}
// Create audit record for results
for i := range results {
a := models.Audit{
UserId: UserId,
SecretId: results[i].SecretId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("User '%s' retrieved SecretId %d", safeList[0].User.UserName, results[i].SecretId),
}
a.AuditLogAdd()
}
// output results as json
c.JSON(http.StatusOK, gin.H{"message": "success", "data": results, "count": len(results)})
}
func ListSecrets(c *gin.Context) {
var UserId int
var results []ListSecret
//var results []models.Secret
s := models.Secret{}
// Get userId that we stored in the context earlier
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
UserId = val.(int)
}
secretList, err := models.SecretsGetAllowed(&s, UserId)
if err != nil {
errString := fmt.Sprintf("error getting allowed secrets : '%s'", err)
log.Printf("ListSecrets %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// Extract the normal secret fields from the allowed list
for _, secret := range secretList {
results = append(results, ListSecret(secret.Secret))
}
// Create audit record
a := models.Audit{
UserId: UserId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Listed %d secrets accessible to user", len(results)),
}
a.AuditLogAdd()
// output results as json
c.JSON(http.StatusOK, gin.H{"message": "success", "data": results, "count": len(results)})
}

View File

@@ -1,106 +0,0 @@
package controllers
import (
"ccsecrets/models"
"ccsecrets/utils/token"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
type RetrieveInput struct {
DeviceName string `json:"deviceName"`
DeviceCategory string `json:"deviceCategory"`
}
func RetrieveSecret(c *gin.Context) {
var input RetrieveInput
// Validate the input matches our struct
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("RetrieveSecret received JSON input '%v'\n", input)
// Get the user and role id of the requestor
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, err := models.GetUserRoleByID(user_id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Populate fields
s := models.Secret{}
s.RoleId = u.RoleId
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
results, err := models.GetSecrets(&s)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(results) == 1 {
// output results as json
c.JSON(http.StatusOK, gin.H{"message": "success", "data": results})
} else if len(results) > 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "found multiple matching secrets, use retrieveMultiple instead"})
return
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "found no matching secrets"})
return
}
}
func RetrieveMultpleSecrets(c *gin.Context) {
var input RetrieveInput
// Validate the input matches our struct
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("StoreSecret received JSON input '%v'\n", input)
// Get the user and role id of the requestor
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, err := models.GetUserRoleByID(user_id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Populate fields
s := models.Secret{}
s.RoleId = u.RoleId
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
results, err := models.GetSecrets(&s)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// output results as json
c.JSON(http.StatusOK, gin.H{"message": "success", "data": results})
}

502
controllers/storeSecrets.go Normal file
View File

@@ -0,0 +1,502 @@
package controllers
import (
"errors"
"fmt"
"log"
"net/http"
"smt/models"
"github.com/gin-gonic/gin"
)
// bindings are validated by https://github.com/go-playground/validator
type SecretInput struct {
SafeId int `json:"safeId"`
SafeName string `json:"safeName"`
SecretId int `json:"secretId"`
DeviceName string `json:"deviceName"`
DeviceCategory string `json:"deviceCategory"`
UserName string `json:"userName"`
SecretValue string `json:"secretValue"`
}
// CheckSafeAllowed returns the SafeId of an allowed safe containing the secret specified by SafeId or SafeName
func CheckSafeAllowed(UserId int, input SecretInput) (int, error) {
// Check which safes a user is allowed to access
allowedSafes, err := models.UserGetSafesAllowed(UserId)
if err != nil {
errString := fmt.Sprintf("error determining safe access : '%s'", err)
log.Printf("StoreSecret %s\n", errString)
return 0, errors.New(errString)
}
// Make sure that the specified safe is in the list of allowed safes
if len(allowedSafes) == 0 {
errString := "error accessing specified safe"
log.Printf("StoreSecret %s\n", errString)
return 0, errors.New(errString)
} else if len(allowedSafes) == 1 && input.SafeId == 0 && len(input.SafeName) == 0 {
log.Printf("StoreSecret user did not specify safe but has access to only one safe '%d'\n", allowedSafes[0].SafeId)
return allowedSafes[0].SafeId, nil
} else {
for _, safe := range allowedSafes {
if input.SafeId > 0 && len(input.SafeName) > 0 && safe.SafeId == input.SafeId && safe.SafeName == input.SafeName {
return safe.SafeId, nil
} else if input.SafeId > 0 && safe.SafeId == input.SafeId {
return safe.SafeId, nil
} else if len(input.SafeName) > 0 && safe.SafeName == input.SafeName {
return safe.SafeId, nil
}
}
errString := "error accessing specified safe"
log.Printf("StoreSecret %s\n", errString)
return 0, errors.New(errString)
}
}
// TODO update to match UpdateSecret
func StoreSecret(c *gin.Context) {
var err error
var input SecretInput
var UserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON received : " + err.Error()})
return
}
// Perform some input validation
/*
if input.SafeId == 0 && len(input.SafeName) == 0 {
errString := "StoreSecret no safe specified\n"
log.Print(errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
*/
if input.DeviceCategory == "" && input.DeviceName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot store secret with empty deviceName and empty deviceCategory"})
return
}
if len(input.UserName) == 0 || len(input.SecretValue) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot store secret with empty UserName or SecretValue"})
return
}
// Don't log this since it contains plaintext secrets
//log.Printf("StoreSecret received JSON input '%v'\n", input)
// Populate fields
s := models.Secret{}
s.UserName = input.UserName
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
// Get userId that we stored in the context earlier
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
UserId = val.(int)
//log.Printf("user_id: %v\n", user_id)
}
// TODO determine whether this access is readonly or not
safeId, err := CheckSafeAllowed(UserId, input)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
s.SafeId = safeId
// If this secret already exists in the database then generate an error
//checkExists, err := models.GetSecrets(&s, false)
checkExists, err := models.SecretsGetFromMultipleSafes(&s, []int{safeId})
// TODO replace GetSecrets with SecretsGetFromMultipleSafes
if err != nil {
errString := fmt.Sprintf("error checking for existing secret : %s", err)
log.Printf("StoreSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if len(checkExists) > 0 {
log.Printf("StoreSecret not storing secret with '%d' already matching secrets.\n", len(checkExists))
c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret attempting to store secret already defined. Use update API call instead"})
return
}
// Encrypt secret
s.Secret = input.SecretValue
_, err = s.EncryptSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret error encrypting secret : " + err.Error()})
return
}
_, err = s.SaveSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret error saving secret : " + err.Error()})
return
}
// Create audit record
a := models.Audit{
UserId: UserId,
SecretId: s.SecretId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Created Secret Id %d", s.SecretId),
}
a.AuditLogAdd()
c.JSON(http.StatusOK, gin.H{"message": "secret stored successfully", "data": models.SecretRestricted(s)})
}
/*
// CheckUpdateSecretAllowed checks to see if a user has access to the specified secret. If so, the corresponding SafeId is returned
func CheckUpdateSecretAllowed(s *models.Secret, user_id int) (int, error) {
// If user has Admin access then perform update
// If user has normal access to the safe the secret is stored in then perform update
// If matching secret is found in multiple safes then generate error
// If user doesn't have access to the safe the matching secret is in then generate error
// NO. That is too complicated!
// Lets try to make this more simple
// A user can only be in one group
// A group can have permissions on multiple safes
// If a user is an admin they can do user related functions like create users, groups, assign permissions
// But a user has to have a permission that maps the group to the safe in order to perform CRUD operations
// What does a group being an admin give them? All users in that group can do user related function
// Query all safes for secrets matching parameters specified
matchingSecrets, err := models.SecretsSearchAllSafes(s)
if err != nil {
errString := fmt.Sprintf("CheckUpdateSecretAllowed error getting matching secrets : '%s'\n", err)
log.Println(errString)
return 0, errors.New(errString)
}
// Query which safes user has access to
userSafes, err := models.UserGetSafesAllowed(int(user_id))
if err != nil {
errString := fmt.Sprintf("CheckUpdateSecretAllowed error getting safes that user has access to : '%s'\n", err)
log.Println(errString)
return 0, errors.New(errString)
}
if len(matchingSecrets) == 0 {
errString := "CheckUpdateSecretAllowed found zero secrets matching supplied parameters"
log.Println(errString)
return 0, errors.New(errString)
} else if len(matchingSecrets) == 1 {
log.Printf("CheckUpdateSecretAllowed found a single matching secret :\n'%+v'\n", matchingSecrets[0])
// Check to see user is allowed to access the safe holding the secret
for _, secret := range matchingSecrets {
for _, user := range userSafes {
if user.SafeId == secret.SafeId {
return user.SafeId, nil
}
}
}
} else { // Multiple matching secrets are found
log.Printf("CheckUpdateSecretAllowed found multiple matching secrets\n")
matchFound := false
matchingSafeId := 0
for _, secret := range matchingSecrets {
for _, user := range userSafes {
if user.SafeId == secret.SafeId {
log.Printf("CheckUpdateSecretAllowed match found for SafeId '%d':\n'%+v'\n", user.SafeId, secret)
if !matchFound {
matchFound = true
matchingSafeId = user.SafeId
} else {
// Found more than one applicable secret, how do we know which one to update?
errString := "CheckUpdateSecretAllowed found multiple secrets matching supplied parameters, supply more specific parameters"
log.Println(errString)
return 0, errors.New(errString)
}
}
}
}
// only one match was found, so we are safe to return that value
if matchFound {
return matchingSafeId, nil
}
}
return 0, nil
}
*/
func UpdateSecret(c *gin.Context) {
var err error
var input SecretInput
var UserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error binding to input JSON : " + err.Error()})
return
}
log.Printf("UpdateSecret received JSON input '%v'\n", input)
if len(input.SecretValue) == 0 {
errString := "no updated secret specified\n"
log.Printf("UpdateSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// Get userId that we stored in the context earlier
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
UserId = val.(int)
//log.Printf("user_id: %v\n", user_id)
}
// Populate fields
s := models.Secret{}
s.UserName = input.UserName
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
secretList, err := models.SecretsGetAllowed(&s, UserId)
if err != nil {
errString := fmt.Sprintf("error determining secret : '%s'", err)
log.Printf("UpdateSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if len(secretList) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no secret matching search parameters"})
return
} else if len(secretList) == 1 {
// Update secret
//log.Printf("secretList[0]: %v\n", secretList[0])
// Check for readonly access
if secretList[0].Permission.ReadOnly {
errString := "read-only access unable to update secret"
log.Printf("UpdateSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
// Check for correct safe
if input.SafeId > 0 {
if input.SafeId != secretList[0].Secret.SafeId {
// Check if user has access to the new safe
allowedSafes, err := models.UserGetSafesAllowed(UserId)
if err != nil {
errString := fmt.Sprintf("error determining allowed safes : '%s'", err)
log.Printf("UpdateSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
allowedFound := false
for i := range allowedSafes {
if allowedSafes[i].SafeId == input.SafeId {
allowedFound = true
break
}
}
if !allowedFound {
errString := "secret cannot be moved into inaccessible safe"
log.Printf("UpdateSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
log.Printf("UpdateSecret moving secret id '%d' into safe id '%d'\n", secretList[0].SecretId, input.SafeId)
s.SafeId = input.SafeId
}
}
s.SecretId = secretList[0].SecretId
// check for empty fields in the update request and update from the existing record
if s.UserName == "" {
s.UserName = secretList[0].Secret.UserName
}
if s.DeviceCategory == "" {
s.DeviceCategory = secretList[0].DeviceCategory
}
if s.DeviceName == "" {
s.DeviceName = secretList[0].DeviceName
}
// Encrypt secret
s.Secret = input.SecretValue
_, err = s.EncryptSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error encrypting secret : " + err.Error()})
return
}
_, err = s.UpdateSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error saving secret : " + err.Error()})
return
}
// Create audit record
a := models.Audit{
UserId: UserId,
SecretId: s.SecretId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Updated Secret Id %d", s.SecretId),
}
a.AuditLogAdd()
c.JSON(http.StatusOK, gin.H{"message": "secret updated successfully", "data": models.SecretRestricted(s)})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "multiple secrets matched search parameters, be more specific"})
return
}
}
func DeleteSecret(c *gin.Context) {
var err error
var input SecretInput
var UserId int
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "DeleteSecret error binding to input JSON : " + err.Error()})
return
}
log.Printf("DeleteSecret received JSON input '%v'\n", input)
// Get userId that we stored in the context earlier
if val, ok := c.Get("user-id"); !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user"})
return
} else {
UserId = val.(int)
//log.Printf("user_id: %v\n", user_id)
}
// Populate fields
s := models.Secret{}
if input.SecretId > 0 {
s.SecretId = input.SecretId
}
s.UserName = input.UserName
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
secretList, err := models.SecretsGetAllowed(&s, UserId)
if err != nil {
errString := fmt.Sprintf("error getting allowed secrets : '%s'", err)
log.Printf("DeleteSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
if len(secretList) == 0 {
errString := "no secret matching search parameters"
log.Printf("DeleteSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
} else if len(secretList) == 1 {
// Delete secret
log.Printf("secretList[0]: %v\n", secretList[0])
// Check for readonly access
if secretList[0].Permission.ReadOnly {
errString := "read-only access unable to delete secret"
log.Printf("DeleteSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
s.SecretId = secretList[0].SecretId
// check for empty fields in the update request and update from the existing record
if s.UserName == "" {
s.UserName = secretList[0].Secret.UserName
}
if s.DeviceCategory == "" {
s.DeviceCategory = secretList[0].DeviceCategory
}
if s.DeviceName == "" {
s.DeviceName = secretList[0].DeviceName
}
_, err = s.DeleteSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "DeleteSecret error deleting secret : " + err.Error()})
return
}
// Create audit record
a := models.Audit{
UserId: UserId,
SecretId: s.SecretId,
IpAddress: c.ClientIP(),
EventText: fmt.Sprintf("Deleted Secret Id %d", s.SecretId),
}
a.AuditLogAdd()
c.JSON(http.StatusOK, gin.H{"message": "secret deleted successfully"})
} else {
errString := "multiple secrets matched search parameters, be more specific"
log.Printf("DeleteSecret %s\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
}
func SecretCheckSafeAllowed(user_id int, input SecretInput) int {
// Query which safes the current user is allowed to access
// SafeId is by default the same as the safe that the user belongs to
safeList, err := models.UserGetSafesAllowed(user_id)
if err != nil {
log.Printf("SecretCheckSafeAllowed error determining allowed safes for userId %d : '%s'\n", user_id, err)
return 0
}
// Verify user has access to specified safe
for _, safe := range safeList {
if len(input.SafeName) > 0 && safe.SafeName == input.SafeName { // Safe specifed by name
return safe.SafeId
} else if input.SafeId > 0 && safe.SafeId == input.SafeId { // Safe specified by id
return safe.SafeId
} else {
log.Printf("SecretCheckSafeAllowed unexpected\n")
}
}
log.Printf("SecretCheckSafeAllowed didn't find any safes\n")
return 0
/*
if !matchFound {
errString := "no safe specified or no access to specified safe"
log.Println(errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
return
}
*/
}

View File

@@ -1,166 +0,0 @@
package controllers
import (
"ccsecrets/models"
"errors"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
// bindings are validated by https://github.com/go-playground/validator
type StoreInput struct {
RoleId int `json:"roleId"`
DeviceName string `json:"deviceName"`
DeviceCategory string `json:"deviceCategory"`
UserName string `json:"userName" binding:"required"`
SecretValue string `json:"secretValue" binding:"required"`
}
func StoreSecret(c *gin.Context) {
var err error
var input StoreInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Don't log this since it contains plaintext secrets
//log.Printf("StoreSecret received JSON input '%v'\n", input)
// Populate fields
s := models.Secret{}
s.UserName = input.UserName
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
// Default role ID is 1 if not defined
if input.RoleId != 0 {
s.RoleId = input.RoleId
} else {
log.Printf("StoreSecret setting default RoleId of 1\n")
s.RoleId = 1
}
if input.DeviceCategory == "" && input.DeviceName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot store secret with empty deviceName and empty deviceCategory"})
return
}
// If this secret already exists in the database then generate an error
checkExists, err := models.GetSecrets(&s)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(checkExists) > 0 {
log.Printf("StoreSecret not storing secret with '%d' already matching secrets.\n", len(checkExists))
c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret attempting to store secret already defined. API calls for update/delete don't yet exist"})
return
}
// Encrypt secret
s.Secret = input.SecretValue
_, err = s.EncryptSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret error encrypting secret : " + err.Error()})
return
}
_, err = s.SaveSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret error saving secret : " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "secret stored successfully"})
}
func UpdateSecret(c *gin.Context) {
var err error
var input StoreInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error binding to input JSON : " + err.Error()})
return
}
log.Printf("UpdateSecret received JSON input '%v'\n", input)
// Get the user and role id of the requestor
u, err := models.GetUserRoleFromToken(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify that the user role is not readonly
if u.ReadOnly {
c.JSON(http.StatusForbidden, gin.H{"error": "UpdateSecret user role does not permit updates"})
return
}
// Populate fields
s := models.Secret{}
s.UserName = input.UserName
s.DeviceName = input.DeviceName
s.DeviceCategory = input.DeviceCategory
// Default role ID is 1 if not defined
if input.RoleId != 0 {
s.RoleId = input.RoleId
} else {
s.RoleId = 1
}
// Confirm that the secret already exists
checkExists, err := models.GetSecrets(&s)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(checkExists) == 0 {
err = errors.New("UpdateSecret could not find existing secret to update")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
} else if len(checkExists) == 1 {
// Set the secret id with the one retrieved from the database
s.SecretId = checkExists[0].SecretId
// check for empty fields in the update request and update from the existing record
if s.UserName == "" {
s.UserName = checkExists[0].UserName
}
if s.DeviceCategory == "" {
s.DeviceCategory = checkExists[0].DeviceCategory
}
if s.DeviceName == "" {
s.DeviceName = checkExists[0].DeviceName
}
// Encrypt secret
s.Secret = input.SecretValue
_, err = s.EncryptSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error encrypting secret : " + err.Error()})
return
}
_, err = s.UpdateSecret()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "UpdateSecret error saving secret : " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "secret updated successfully"})
} else {
err = errors.New("UpdateSecret found multiple secrets matching input data, be more specific")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}

45
controllers/unlock.go Normal file
View File

@@ -0,0 +1,45 @@
package controllers
import (
"log"
"net/http"
"smt/models"
"github.com/gin-gonic/gin"
)
type UnlockInput struct {
SecretKey string `json:"secretKey"`
}
// Unlock receives secret key and store it in memory
func Unlock(c *gin.Context) {
var input UnlockInput
// Validate the input matches our struct
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Println("Unlock received JSON input")
if models.CheckKeyProvided() {
c.JSON(http.StatusBadRequest, gin.H{"error": "secret key can only be provided once after service start"})
return
}
// check that the key is 32 bytes long
if len(input.SecretKey) != 32 {
c.JSON(http.StatusBadRequest, gin.H{"error": "secret key provided is invalid, must be exactly 32 bytes long"})
return
}
err := models.ReceiveKey(input.SecretKey)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err})
return
}
// output results as json
c.JSON(http.StatusOK, gin.H{"message": "success", "data": "secret key successfully processed"})
}

77
go.mod
View File

@@ -1,52 +1,57 @@
module ccsecrets
module smt
go 1.19
go 1.24.4
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.9.0
github.com/jmoiron/sqlx v1.3.5
github.com/gin-gonic/gin v1.10.1
github.com/go-ldap/ldap/v3 v3.4.11
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.7.0
modernc.org/sqlite v1.21.0
golang.org/x/crypto v0.39.0
modernc.org/sqlite v1.38.0
)
require (
github.com/bytedance/sonic v1.8.6 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.12.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
modernc.org/gc/v3 v3.1.0 // indirect
modernc.org/libc v1.66.2 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/strutil v1.2.1 // indirect
modernc.org/token v1.1.0 // indirect
)

262
go.sum
View File

@@ -1,132 +1,216 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.6 h1:aUgO9S8gvdN6SyW2EhIpAw5E4ChworywIEndZCkCVXk=
github.com/bytedance/sonic v1.8.6/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/gc/v3 v3.1.0 h1:CiObI+9ROz7pjjH3iAgMPaFCN5zE3sN5KF4jet8BWdc=
modernc.org/gc/v3 v3.1.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
modernc.org/libc v1.66.2 h1:JCBxlJzZOIwZY54fzjHN3Wsn8Ty5PUTPr/xioRkmecI=
modernc.org/libc v1.66.2/go.mod h1:ceIGzvXxP+JV3pgVjP9avPZo6Chlsfof2egXBH3YT5Q=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

254
index.htm Normal file
View File

@@ -0,0 +1,254 @@
<h1>Secrets Management Tool (SMT)</h1>
<p>Build Date: <code>{BUILDTIME}</code></p>
<p>Build Hash: <code>{SHA1VER}</code></p>
<h2>Overview</h2>
<p>Provide REST API for CRUD to store and retrieve secrets with associated username, device name and optionally device class. Secret is stored in sqlite database once encrypted using an AES256 block cipher wrapped in Galois Counter Mode with the standard nonce length.</p>
<p>All secret operations (Create, Read, Update or Delete) require successful authentication. A JWT token is returned upon login, which must be provided for all other operations.</p>
<p>Multiple user roles are supported, with each user only able to access secrets matching their user role. One exception is the built in administrator role that is able to access all secrets.</p>
<p>Written by Nathan Coad (nathan.coad@dell.com)</p>
<h2>Installation</h2>
<ol>
<li>Copy binary to chosen location, eg /srv/smt/smt</li>
<li>Create .env file in same directory as binary, populate as per Configuration section below</li>
<li>Create systemd service definition</li>
<li>Start service</li>
</ol>
<h2>Configuration</h2>
<table>
<thead>
<tr>
<th>Environment Variable Name</th>
<th>Description</th>
<th>Example</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td>LOG_FILE</td>
<td>Specify the name/path of file to write log messages to</td>
<td>/var/log/smt.log</td>
<td>./smt.log</td>
</tr>
<tr>
<td>BIND_IP</td>
<td>Specify the local IP address to bind to.</td>
<td>127.0.0.1</td>
<td>Primary IPv4 address</td>
</tr>
<tr>
<td>BIND_PORT</td>
<td>Specify the TCP/IP port to bind to.</td>
<td>443</td>
<td>8443</td>
</tr>
<tr>
<td>TLS_KEY_FILE</td>
<td>Specify the filename of the TLS certificate private key (must be unencrypted) in PEM format</td>
<td>key.pem</td>
<td>privkey.pem</td>
</tr>
<tr>
<td>TLS_CERT_FILE</td>
<td>Specify the filename of the TLS certificate file in PEM format</td>
<td>cert.pem</td>
<td>cert.pem</td>
</tr>
<tr>
<td>TOKEN_HOUR_LIFESPAN</td>
<td>Number of hours that the JWT token returned at login is valid</td>
<td>12</td>
<td>No default specified, must define this value</td>
</tr>
<tr>
<td>API_SECRET</td>
<td>Secret to use when generating JWT token</td>
<td>3c55990bd479322e2053db3a8</td>
<td>No default specified, must define this value</td>
</tr>
<tr>
<td>INITIAL_PASSWORD</td>
<td>Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash</td>
<td>$2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce</td>
<td>password</td>
</tr>
<tr>
<td>SECRETS_KEY</td>
<td>Key to use for AES256 GCM encryption. Must be exactly 32 bytes</td>
<td>AES256Key-32Characters1234567890</td>
<td>No default specified, must define this value or use /api/unlock at runtime</td>
</tr>
</tbody>
</table>
<p>If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period.</p>
<p>Example for generating API_SECRET and SECRETS_KEY is the following command on linux: <code>head /dev/urandom | tr -dc A-Za-z0-9 | head -c32</code></p>
<h2>Systemd script</h2>
<p>Create/update the systemd service definition at /etc/systemd/system/smt.service and then run systemctl daemon-reload</p>
<pre><code>[Unit]
Description=Secrets Management Tool
After=network.target
#StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=root
ExecStart=/srv/smt/smt
[Install]
WantedBy=multi-user.target
</code></pre>
<h2>API</h2>
<h3>Unlock</h3>
<p>POST <code>/api/unlock</code></p>
<p>Data</p>
<pre><code>{
&quot;secretKey&quot;: &quot;Example32ByteSecretKey0123456789&quot;
}
</code></pre>
<p>If the SECRETS_KEY environment variable is not defined, this API call to unlock stored secrets must be performed after initial startup of SMT. Storing/retrieval of secrets will not succeed until this API call has been made.</p>
<h3>User Operations</h3>
<h4>Register</h4>
<p>POST <code>/api/admin/register</code></p>
<p>Data</p>
<pre><code>{
&quot;username&quot;: &quot;&quot;,
&quot;password&quot;: &quot;&quot;,
&quot;RoleId&quot;: 2
}
</code></pre>
<p>This operation can only be performed by a user with a role that is admin enabled. There are 3 built in roles, which can be viewed via the <code>/api/admin/roles</code> endpoint.</p>
<h4>Login</h4>
<p>POST <code>/api/login</code></p>
<p>Data</p>
<pre><code>{
&quot;username&quot;: &quot;&quot;,
&quot;password&quot;: &quot;&quot;
}
</code></pre>
<p>This API call will return a JWT token that must be present for any other API calls to succeed. The validity duration of this token is based on the configured TOKEN_HOUR_LIFESPAN value. JWT token is returned as value of <code>access_token</code>, and must be supplied via a HTTP header in the form <code>&quot;Authorization: Bearer &lt;JWT_TOKEN&gt;&quot;</code> for all subsequent API calls.</p>
<h4>List Roles</h4>
<p>GET <code>/api/admin/roles</code></p>
<p>This operation can only be performed by a user with a role that is admin enabled. Lists currently defined roles.</p>
<h4>List Users</h4>
<p>GET <code>/api/admin/users</code></p>
<p>This operation can only be performed by a user with a role that is admin enabled. Lists currently defined users.</p>
<h3>Secrets Operations</h3>
<h4>Store</h4>
<p>POST <code>/api/secret/store</code></p>
<p>Data</p>
<pre><code>{
&quot;deviceName&quot;: &quot;&quot;,
&quot;deviceCategory&quot;: &quot;&quot;,
&quot;userName&quot;: &quot;&quot;,
&quot;secretValue&quot;: &quot;&quot;
}
</code></pre>
<p>Must be logged in to execute this command. Role of current user cannot be a ReadOnly role. Secret will be stored with the RoleId of the currently logged in user. Either deviceName or deviceCategory can be blank but not both.</p>
<p>If a secret exists for this RoleId and matching deviceName and deviceCategory then an error will be generated.</p>
<h4>Retrieve</h4>
<p>POST <code>/api/secret/retrieve</code></p>
<p>Data</p>
<pre><code>{
&quot;deviceName&quot;: &quot;&quot;,
&quot;deviceCategory&quot;: &quot;&quot;,
&quot;userName&quot;: &quot;&quot;
}
</code></pre>
<p>Must be logged in to execute this command. Only secrets registered with the current user&rsquo;s RoleId can be retrieved.</p>
<p>Either deviceName or deviceCategory can be specified (or both). Wildcards are supported for both deviceName and deviceCategory fields. userName can also be specified in conjunction with deviceName or deviceCategory.
1. The percent sign % wildcard matches any sequence of zero or more characters.
2. The underscore _ wildcard matches any single character.</p>
<p>GET <code>/api/secret/retrieve/name/&lt;searchname&gt;</code></p>
<p>Search for a secret specified by deviceName using a GET request.
Must be logged in to execute this command. Only secrets registered with the current user&rsquo;s RoleId can be retrieved.</p>
<p>GET <code>/api/secret/retrieve/category/&lt;searchname&gt;</code></p>
<p>Search for a secret specified by deviceCategory using a GET request.
Must be logged in to execute this command. Only secrets registered with the current user&rsquo;s RoleId can be retrieved.</p>
<h4>Update</h4>
<p>POST <code>/api/secret/update</code></p>
<p>Data</p>
<pre><code>{
&quot;deviceName&quot;: &quot;&quot;,
&quot;deviceCategory&quot;: &quot;&quot;,
&quot;userName&quot;: &quot;&quot;,
&quot;secretValue&quot;: &quot;&quot;
}
</code></pre>
<p>Users with ReadOnly role will receive Forbidden error when calling this API endpoint. The values specified in deviceName and deviceCategory must match exactly one existing secret record for the RoleId of the currently logged in user. Wildcards are supported for deviceName and deviceCategory.</p>
<h4>List</h4>
<p>GET <code>/api/secret/list</code></p>
<p>Will generate a list of device names and categories but not secret data.</p>

187
main.go
View File

@@ -1,17 +1,21 @@
package main
import (
"ccsecrets/controllers"
"ccsecrets/middlewares"
"ccsecrets/models"
"ccsecrets/utils"
"bytes"
"context"
"crypto/tls"
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"runtime"
"smt/controllers"
"smt/middlewares"
"smt/models"
"smt/utils"
"syscall"
"time"
@@ -23,7 +27,81 @@ import (
var sha1ver string // sha1 revision used to build the program
var buildTime string // when the executable was built
type Replacements map[string]string
var replacements = make(Replacements)
//go:embed www/*
var staticContent embed.FS
func getAllFilenames(efs *embed.FS) (files []string, err error) {
if err := fs.WalkDir(efs, ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}
files = append(files, path)
return nil
}); err != nil {
return nil, err
}
return files, nil
}
// staticFileServer serves files from the provided fs.FS
func staticFileServer(content embed.FS) gin.HandlerFunc {
return func(c *gin.Context) {
// Get the requested file name
fileName := c.Request.URL.Path
// Root request should load index.htm
if len(fileName) == 1 {
//log.Printf("staticFileServer replacing root request with index.html")
fileName = "www/index.html"
} else {
//fileName = strings.TrimLeft(fileName, "/")
fileName = "www" + fileName
}
//log.Printf("staticFileServer attempting to load filename '%s'\n", fileName)
// Try to open the file from the embedded FS
file, err := content.Open(fileName)
if err != nil {
log.Printf("staticFileServer error opening '%s' : '%s'\n", fileName, err)
c.String(http.StatusNotFound, "File not found")
return
}
defer file.Close()
// Serve the file contents
fileStat, _ := file.Stat()
data := make([]byte, fileStat.Size())
_, err = file.Read(data)
if err != nil {
c.String(http.StatusInternalServerError, "Error reading file")
return
}
// parse the file and perform text replacements as necessary
for key, element := range replacements {
//log.Printf("Searching for '%s' to replace\n", key)
data = bytes.Replace(data, []byte(key), []byte(element), -1)
}
// Set the proper Content-Type header based on file extension
c.Header("Content-Type", http.DetectContentType(data))
c.Data(http.StatusOK, "", data)
}
}
func main() {
// These replacements are for the embedded html generated from README.md
replacements["{SHA1VER}"] = sha1ver
replacements["{BUILDTIME}"] = buildTime
replacements["{RUNTIME}"] = runtime.Version()
// Load data from environment file
envFilename := utils.GetFilePath(".env")
@@ -45,11 +123,38 @@ func main() {
}
log.SetOutput(logfileWriter)
log.Printf("SMT starting execution. Built on %s from sha1 %s\n", buildTime, sha1ver)
log.Printf("SMT starting execution. Built on %s from sha1 %s. Runtime %s\n", buildTime, sha1ver, runtime.Version())
/*
// for debugging, list all the files that we embedded at compile time
files, err := getAllFilenames(&staticContent)
if err != nil {
log.Printf("Unable to access embedded fs : '%s'\n", err)
}
for i := range files {
log.Printf("Embedded file : '%s'\n", files[i])
}
*/
// Initiate connection to sqlite and make sure our schema is up to date
models.ConnectDatabase()
// Set secrets key from .env file
keyString := os.Getenv("SECRETS_KEY")
if keyString != "" {
// Key was defined in environment variable, let the models package know our secrets key
log.Println("Found secret key in environment variable")
models.ReceiveKey(keyString)
}
// If LDAP configuration is defined, prepare connection
ldapServer := os.Getenv("LDAP_BIND_ADDRESS")
if ldapServer != "" {
models.LdapSetup()
}
// Create context that listens for the interrupt signal from the OS.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
@@ -72,11 +177,6 @@ func main() {
// Recovery middleware recovers from any panics and writes a 500 if there was one.
router.Use(gin.Recovery())
// TODO - think of a better default landing page
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, fmt.Sprintf("SMT Built on %s from sha1 %s\n", buildTime, sha1ver))
})
// Set some options for TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
@@ -101,7 +201,7 @@ func main() {
// Determine bind port
bindPort := os.Getenv("BIND_PORT")
if bindPort == "" {
bindIP = "8443"
bindPort = "8443"
}
bindAddress := fmt.Sprint(bindIP, ":", bindPort)
log.Printf("Will listen on address 'https://%s'\n", bindAddress)
@@ -133,6 +233,9 @@ func main() {
TLSConfig: tlsConfig,
}
// Serve the embedded HTML file if no other routes match
router.NoRoute(staticFileServer(staticContent))
// Register our routes
public := router.Group("/api")
public.POST("/login", controllers.Login)
@@ -140,16 +243,62 @@ func main() {
// API calls that only an administrator can make
adminOnly := router.Group("/api/admin")
adminOnly.Use(middlewares.JwtAuthAdminMiddleware())
adminOnly.POST("/register", controllers.Register)
adminOnly.GET("/roles", controllers.GetRoles)
// User functions for admin
adminOnly.POST("/user/delete", controllers.DeleteUser)
adminOnly.POST("/user/register", controllers.AddUser) // TODO deprecate
adminOnly.POST("/user/add", controllers.AddUser)
adminOnly.GET("/users", controllers.GetUsers)
// TODO
//adminOnly.POST("/user/update", controllers.UpdateUserHandler)
// Group functions for admin
adminOnly.GET("/groups", controllers.GetGroupsHandler)
adminOnly.POST("/group/add", controllers.AddGroupHandler)
// TODO
//adminOnly.POST("/group/update", controllers.UpdateGroupHandler)
adminOnly.POST("/group/delete", controllers.DeleteGroupHandler)
// Permission functions for admin
adminOnly.GET("/permissions", controllers.GetPermissionsHandler)
adminOnly.POST("/permission/add", controllers.AddPermissionHandler)
adminOnly.POST("/permission/delete", controllers.DeletePermissionHandler)
adminOnly.POST("/permission/update", controllers.UpdatePermissionHandler)
// Safe functions for admin
adminOnly.GET("/safe/listall", controllers.GetAllSafesHandler)
adminOnly.POST("/safe/add", controllers.AddSafeHandler)
adminOnly.POST("/safe/delete", controllers.DeleteSafeHandler)
// Other functions for admin
adminOnly.POST("/unlock", controllers.Unlock)
adminOnly.GET("/logs", controllers.GetAuditLogsHandler)
// TODO
//adminOnly.GET("/logs/secret/:id", controllers.GetAuditLogsBySecretHandler)
//adminOnly.GET("/logs/user/:id", controllers.GetAuditLogsByUserHandler)
// Get secrets
protected := router.Group("/api/secret")
protected.Use(middlewares.JwtAuthMiddleware())
protected.GET("/retrieve", controllers.RetrieveSecret)
protected.GET("/retrieveMultiple", controllers.RetrieveMultpleSecrets)
protected.POST("/store", controllers.StoreSecret)
protected.POST("/update", controllers.UpdateSecret)
secretRoutes := router.Group("/api/secret")
secretRoutes.Use(middlewares.JwtAuthMiddleware())
secretRoutes.POST("/retrieve", controllers.RetrieveSecret) // TODO deprecate, replace retrieve with get
secretRoutes.POST("/get", controllers.RetrieveSecret)
secretRoutes.GET("/list", controllers.ListSecrets)
secretRoutes.POST("/store", controllers.StoreSecret) // TODO deprecate, replace store with add
secretRoutes.POST("/add", controllers.StoreSecret)
secretRoutes.POST("/update", controllers.UpdateSecret)
secretRoutes.POST("/delete", controllers.DeleteSecret)
// Get Safes (only those user allowed to access)
safeRoutes := router.Group("/api/safe")
safeRoutes.Use(middlewares.JwtAuthMiddleware())
safeRoutes.GET("/list", controllers.GetSafesHandler)
// Support parameters in path
// See https://gin-gonic.com/docs/examples/param-in-path/
secretRoutes.GET("/retrieve/name/:devicename", controllers.RetrieveSecretByDevicename)
secretRoutes.GET("/retrieve/category/:devicecategory", controllers.RetrieveSecretByDevicecategory)
secretRoutes.GET("/retrieve/user/:username", controllers.RetrieveSecretByUsername)
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below

View File

@@ -4,8 +4,8 @@ import (
"log"
"net/http"
"ccsecrets/models"
"ccsecrets/utils/token"
"smt/models"
"smt/utils/token"
"github.com/gin-gonic/gin"
)
@@ -14,10 +14,24 @@ func JwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := token.TokenValid(c)
if err != nil {
log.Printf("JwtAuthMiddleware token is not valid : '%s'\n", err)
c.String(http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
}
// Token is valid, extract user_id
user_id, err := token.ExtractTokenID(c)
if err != nil {
log.Printf("JwtAuthMiddleware user_id could not be parsed : '%s'\n", err)
c.String(http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
}
// Store user id in context for accessing later
//log.Printf("JwtAuthMiddleware storing user-id '%d'\n", user_id)
c.Set("user-id", int(user_id))
c.Next()
}
}
@@ -25,10 +39,9 @@ func JwtAuthMiddleware() gin.HandlerFunc {
func JwtAuthAdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// TODO - also verify user role of admin
err := token.TokenValid(c)
if err != nil {
log.Printf("JwtAuthAdminMiddleware token is not valid : '%s'\n", err)
c.String(http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
@@ -38,21 +51,48 @@ func JwtAuthAdminMiddleware() gin.HandlerFunc {
user_id, err := token.ExtractTokenID(c)
if err != nil {
log.Printf("JwtAuthAdminMiddleware could not extract user ID from context : '%s'\n", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.Abort()
return
}
//log.Printf("JwtAuthAdminMiddleware determined user id as '%v'\n", user_id)
c.Set("user-id", int(user_id))
ur, err := models.GetUserRoleByID(user_id)
/*
//user_id := c.GetInt("user-id")
var user_id int
if val, ok := c.Get("user-id"); !ok {
log.Printf("JwtAuthAdminMiddleware : user-id not in context. Keys : '%+v'\n", c.Keys)
c.JSON(http.StatusBadRequest, gin.H{"error": "error determining user id"})
return
} else {
user_id = val.(int)
}
if user_id == 0 {
errString := "could not extract user ID from context"
log.Printf("JwtAuthAdminMiddleware '%s'\n", errString)
c.JSON(http.StatusBadRequest, gin.H{"error": errString})
c.Abort()
return
}
*/
// TODO determine user role
//ur, err := models.GetUserRoleByID(user_id)
ug, err := models.UserGetGroupByID(uint(user_id))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.Abort()
return
}
log.Printf("JwtAuthAdminMiddleware retrieved UserRole object '%v'\n", ur)
log.Printf("JwtAuthAdminMiddleware retrieved UserGroup object for UserId '%d'\n", ug.UserId)
if !ur.Admin {
// Verify that the user has a role with the admin flag set
if !ug.Admin {
c.String(http.StatusUnauthorized, "User role is Non-Admin")
c.Abort()
return

68
models/audit.go Normal file
View File

@@ -0,0 +1,68 @@
package models
import (
"log"
"time"
)
// Define audit functions
type Audit struct {
AuditId int `db:"AuditId" json:"auditId"`
UserId int `db:"UserId" json:"userId"`
SecretId int `db:"SecretId" json:"secretId"`
EventText string `db:"EventText" json:"eventText"`
EventTime time.Time `db:"EventTime" json:"eventTime"`
IpAddress string `db:"IpAddress" json:"ipAddress"`
}
// AuditLogAdd adds a new audit record to the database
func (a *Audit) AuditLogAdd() (*Audit, error) {
var err error
// Populate timestamp field if not already set
if a.EventTime.IsZero() {
a.EventTime = time.Now().UTC()
}
result, err := db.NamedExec(("INSERT INTO audit (UserId, SecretId, EventText, EventTime, IpAddress) VALUES (:UserId, :SecretId, :EventText, :EventTime, :IpAddress);"), a)
if err != nil {
log.Printf("AuditLogAdd error executing sql record : '%s'\n", err)
return &Audit{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
a.AuditId = int(id)
log.Printf("AuditLogAdd insert returned result id '%d' affecting %d row(s).\n", id, affected)
}
return a, nil
}
// AuditList returns a list of all audit logs in database
func AuditLogList() ([]Audit, error) {
var results []Audit
// Query database for groups
rows, err := db.Queryx("SELECT * FROM audit")
if err != nil {
log.Printf("AuditLogList error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var a Audit
err = rows.StructScan(&a)
if err != nil {
log.Printf("AuditLogList error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, a)
}
log.Printf("AuditLogList retrieved '%d' results\n", len(results))
}
return results, nil
}

546
models/db.go Normal file
View File

@@ -0,0 +1,546 @@
package models
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"smt/utils"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
var db *sqlx.DB
const (
sqlFile = "smt.db"
)
const createUsers string = `
CREATE TABLE IF NOT EXISTS users (
UserId INTEGER PRIMARY KEY AUTOINCREMENT,
GroupId INTEGER,
UserName VARCHAR,
Password VARCHAR,
Admin BOOLEAN DEFAULT 0,
LdapUser BOOLEAN DEFAULT 0,
LastLogin datetime DEFAULT (datetime('1970-01-01 00:00:00'))
);
`
const createSafes string = `
CREATE TABLE IF NOT EXISTS safes (
SafeId INTEGER PRIMARY KEY AUTOINCREMENT,
SafeName VARCHAR
);
`
const createGroups string = `
CREATE TABLE IF NOT EXISTS groups (
GroupId INTEGER PRIMARY KEY AUTOINCREMENT,
GroupName VARCHAR,
LdapGroup BOOLEAN DEFAULT 0,
LdapDn VARCHAR DEFAULT '',
Admin BOOLEAN DEFAULT 0
);
`
const createPermissions = `
CREATE TABLE IF NOT EXISTS permissions (
PermissionId INTEGER PRIMARY KEY AUTOINCREMENT,
Description VARCHAR DEFAULT '',
ReadOnly BOOLEAN DEFAULT 0,
SafeId INTEGER,
UserId INTEGER DEFAULT 0,
GroupId INTEGER DEFAULT 0,
FOREIGN KEY (SafeId) REFERENCES safes(SafeId)
);
`
const createSecrets string = `
CREATE TABLE IF NOT EXISTS secrets (
SecretId INTEGER PRIMARY KEY AUTOINCREMENT,
SafeId INTEGER,
DeviceName VARCHAR,
DeviceCategory VARCHAR,
UserName VARCHAR,
Secret VARCHAR,
LastUpdated datetime DEFAULT (datetime('1970-01-01 00:00:00')),
FOREIGN KEY (SafeId) REFERENCES safes(SafeId)
);
`
const createSchema string = `
CREATE TABLE IF NOT EXISTS schema (
Version INTEGER
);
`
const createAudit string = `
CREATE TABLE IF NOT EXISTS audit (
AuditId INTEGER PRIMARY KEY AUTOINCREMENT,
UserId INTEGER DEFAULT 0,
SecretId INTEGER DEFAULT 0,
EventText VARCHAR DEFAULT '',
IpAddress VARCHAR DEFAULT '',
EventTime datetime DEFAULT (datetime('1970-01-01 00:00:00'))
);
`
// Establish connection to sqlite database
func ConnectDatabase() {
var err error
// Try using sqlite as our database
sqlPath := utils.GetFilePath(sqlFile)
db, err = sqlx.Open("sqlite", sqlPath)
if err != nil {
log.Printf("Error opening sqlite database connection to file '%s' : '%s'\n", sqlPath, err)
os.Exit(1)
} else {
log.Printf("Connected to sqlite database file '%s'\n", sqlPath)
}
//sqlx.NameMapper = func(s string) string { return s }
// Make sure our tables exist
CreateTables()
//defer db.Close()
}
func DisconnectDatabase() {
log.Printf("DisconnectDatabase called")
defer db.Close()
}
func CreateTables() {
var err error
var rowCount int
// Create database tables if it doesn't exist
// groups table
if _, err = db.Exec(createGroups); err != nil {
log.Printf("Error checking groups table : '%s'", err)
os.Exit(1)
}
// Add initial groups
rowCount, _ = CheckCount("groups")
if rowCount == 0 {
if _, err = db.Exec("INSERT INTO groups (GroupId, GroupName, Admin) VALUES(1, 'Administrators', 1);"); err != nil {
log.Printf("Error adding initial group entry id 1 : '%s'", err)
os.Exit(1)
}
if _, err = db.Exec("INSERT INTO groups (GroupId, GroupName, Admin) VALUES(2, 'Users', 0);"); err != nil {
log.Printf("Error adding initial group entry id 2 : '%s'", err)
os.Exit(1)
}
}
// Users table
if _, err = db.Exec(createUsers); err != nil {
log.Printf("Error checking users table : '%s'", err)
os.Exit(1)
}
rowCount, _ = CheckCount("users")
if rowCount == 0 {
// Check if there was an initial password defined in the .env file
initialPassword := os.Getenv("INITIAL_PASSWORD")
if initialPassword == "" {
initialPassword = "password"
} else if initialPassword[:4] == "$2a$" {
log.Printf("CreateTables inital admin password is already a hash")
} else {
cryptText, _ := bcrypt.GenerateFromPassword([]byte(initialPassword), bcrypt.DefaultCost)
initialPassword = string(cryptText)
}
if _, err = db.Exec("INSERT INTO users (UserId, GroupId, UserName, Password, LdapUser, Admin) VALUES(1, 1, 'Administrator', ?, false, true);", initialPassword); err != nil {
log.Printf("Error adding initial admin role : '%s'", err)
os.Exit(1)
}
if _, err = db.Exec("INSERT INTO users (UserId, GroupId, UserName, Password, LdapUser, Admin) VALUES(2, 2, 'User', ?, false, false);", initialPassword); err != nil {
log.Printf("Error adding initial admin role : '%s'", err)
os.Exit(1)
}
}
// Safes table
if _, err = db.Exec(createSafes); err != nil {
log.Printf("Error checking safes table : '%s'", err)
os.Exit(1)
}
// Create an initial safe
rowCount, _ = CheckCount("safes")
if rowCount == 0 {
if _, err = db.Exec("INSERT INTO safes VALUES(1, 'Default Safe');"); err != nil {
log.Printf("Error adding initial safe entry : '%s'", err)
os.Exit(1)
}
}
// Secrets table
if _, err = db.Exec(createSecrets); err != nil {
log.Printf("Error checking secrets table : '%s'", err)
os.Exit(1)
}
// permissions table
if _, err = db.Exec(createPermissions); err != nil {
log.Printf("Error checking permissions table : '%s'", err)
os.Exit(1)
}
// Add initial permissions
rowCount, _ = CheckCount("permissions")
if rowCount == 0 {
if _, err = db.Exec("INSERT INTO permissions (Description, ReadOnly, GroupId, SafeId) VALUES('Default Admin Group Permission', false, 1, 1);"); err != nil {
log.Printf("Error adding initial permissions entry userid 1 : '%s'", err)
os.Exit(1)
}
if _, err = db.Exec("INSERT INTO permissions (Description, ReadOnly, SafeId, GroupId) VALUES('Default User Group Permission', false, 1, 2);"); err != nil {
log.Printf("Error adding initial permissions entry userid 2 : '%s'", err)
os.Exit(1)
}
}
// Schema table should go last so we know if the database has a value in the schema table then everything was created properly
if _, err = db.Exec(createSchema); err != nil {
log.Printf("Error checking schema table : '%s'", err)
os.Exit(1)
}
schemaCheck, _ := CheckColumnExists("schema", "Version")
if !schemaCheck {
if _, err = db.Exec("INSERT INTO schema VALUES(2);"); err != nil {
log.Printf("Error adding initial schema version : '%s'", err)
os.Exit(1)
}
}
// Audit log table
if _, err = db.Exec(createAudit); err != nil {
log.Printf("Error checking audit table : '%s'", err)
os.Exit(1)
}
// Check the database schema version
version, _ := GetSchemaVersion()
if version >= 3 {
log.Printf("Database schema up to date\n")
} else {
// Remove users RoleId column
userRoleIdCheck, _ := CheckColumnExists("users", "RoleId")
if userRoleIdCheck {
//_, err := db.Exec("ALTER TABLE users DROP COLUMN RoleId;")
_, err := db.Exec(`
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
ALTER TABLE users RENAME TO _users_old;
CREATE TABLE users
(
UserId INTEGER PRIMARY KEY AUTOINCREMENT,
GroupId INTEGER,
UserName VARCHAR,
Password VARCHAR,
Admin BOOLEAN DEFAULT 0,
LdapUser BOOLEAN DEFAULT 0
);
INSERT INTO users SELECT * FROM _users_old;
COMMIT;
PRAGMA foreign_keys=on;
DROP TABLE _users_old;
`)
if err != nil {
log.Printf("Error altering users table to drop RoleId column : '%s'\n", err)
os.Exit(1)
}
}
// Set any unassigned secrets to the default safe id
if _, err = db.Exec("UPDATE users SET LdapUser = 0 WHERE LdapUser is null;"); err != nil {
log.Printf("Error setting LdapUser flag to false for existing users : '%s'", err)
os.Exit(1)
}
// Remove LdapGroup column from roles table
ldapCheck, _ := CheckColumnExists("roles", "LdapGroup")
if ldapCheck {
_, err := db.Exec("ALTER TABLE roles DROP COLUMN LdapGroup;")
if err != nil {
log.Printf("Error altering roles table to renmove LdapGroup column : '%s'\n", err)
os.Exit(1)
}
}
// Add SafeId column to secrets table
safeIdCheck, _ := CheckColumnExists("secrets", "SafeId")
if !safeIdCheck {
// Add the column for LdapGroup in the roles table
_, err := db.Exec("ALTER TABLE secrets ADD COLUMN SafeId INTEGER REFERENCES safes(SafeId);")
if err != nil {
log.Printf("Error altering secrets table to add SafeId column : '%s'\n", err)
os.Exit(1)
}
}
// Set any unassigned secrets to the default safe id
if _, err = db.Exec("UPDATE secrets SET SafeId = 1 WHERE SafeId is null;"); err != nil {
log.Printf("Error setting safe ID of existing secrets : '%s'", err)
os.Exit(1)
}
// Remove RoleId column from secrets table
secretsRoleIdCheck, _ := CheckColumnExists("secrets", "RoleId")
if secretsRoleIdCheck {
_, err := db.Exec(`
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
ALTER TABLE secrets RENAME TO _secrets_old;
CREATE TABLE secrets
(
SecretId INTEGER PRIMARY KEY AUTOINCREMENT,
RoleId INTEGER,
SafeId INTEGER,
DeviceName VARCHAR,
DeviceCategory VARCHAR,
UserName VARCHAR,
Secret VARCHAR,
FOREIGN KEY (SafeId) REFERENCES safes(SafeId)
);
INSERT INTO secrets SELECT SecretId, RoleId, SafeId, DeviceName, DeviceCategory, UserName, Secret FROM _secrets_old;
ALTER TABLE secrets DROP COLUMN RoleId;
ALTER TABLE secrets ADD COLUMN LastUpdated datetime;
UPDATE secrets SET LastUpdated = (datetime('1970-01-01 00:00:00')) WHERE LastUpdated is null;
COMMIT;
PRAGMA foreign_keys=on;
DROP TABLE _secrets_old;
`)
if err != nil {
log.Printf("Error altering secrets table to remove RoleId column : '%s'\n", err)
os.Exit(1)
}
}
// Remove the Admin column from roles table
rolesAdminCheck, _ := CheckColumnExists("roles", "Admin")
if rolesAdminCheck {
_, err := db.Exec("ALTER TABLE roles DROP COLUMN Admin;")
if err != nil {
log.Printf("Error altering roles table to remove Admin column : '%s'\n", err)
os.Exit(1)
}
}
// Remove the RoleId from permissiosn table
permissionsRoleIdCheck, _ := CheckColumnExists("permissions", "RoleId")
if permissionsRoleIdCheck {
_, err := db.Exec(`
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
ALTER TABLE permissions RENAME TO _permissions_old;
CREATE TABLE permissions
(
PermissionId INTEGER PRIMARY KEY AUTOINCREMENT,
Description VARCHAR DEFAULT '',
ReadOnly BOOLEAN DEFAULT 0,
SafeId INTEGER,
UserId INTEGER DEFAULT 0,
GroupId INTEGER DEFAULT 0,
FOREIGN KEY (SafeId) REFERENCES safes(SafeId)
);
INSERT INTO permissions SELECT PermissionId, SafeId, UserId, GroupId, '' AS Description, 0 as ReadOnly FROM _permissions_old;
UPDATE permissions SET ReadOnly = 0 WHERE ReadOnly is null;
UPDATE permissions SET Description = '' WHERE Description is null;
UPDATE permissions SET UserId = 0 WHERE UserId is null;
UPDATE permissions SET GroupId = 0 WHERE GroupId is null;
COMMIT;
PRAGMA foreign_keys=on;
DROP TABLE _permissions_old;
`)
if err != nil {
log.Printf("Error altering permissions table to remove RoleId column : '%s'\n", err)
os.Exit(1)
}
}
secretsLastUpdatedCheck, _ := CheckColumnExists("secrets", "LastUpdated")
if !secretsLastUpdatedCheck {
// Add the column for LastUpdated in the secrets table
_, err := db.Exec("ALTER TABLE secrets ADD COLUMN LastUpdated datetime;")
if err != nil {
log.Printf("Error altering secrets table to add LastUpdated column : '%s'\n", err)
os.Exit(1)
}
// Set the default value
if _, err = db.Exec("UPDATE secrets SET LastUpdated = (datetime('1970-01-01 00:00:00')) WHERE LastUpdated is null;"); err != nil {
log.Printf("Error setting LastUpdated of existing secrets : '%s'", err)
os.Exit(1)
}
}
lastLoginCheck, _ := CheckColumnExists("users", "LastLogin")
if !lastLoginCheck {
// Add the column for LastUpdated in the secrets table
_, err := db.Exec("ALTER TABLE users ADD COLUMN LastLogin datetime;")
if err != nil {
log.Printf("Error altering users table to add LastLogin column : '%s'\n", err)
os.Exit(1)
}
// Set the default value
if _, err = db.Exec("UPDATE users SET LastLogin = (datetime('1970-01-01 00:00:00')) WHERE LastLogin is null;"); err != nil {
log.Printf("Error setting LastLogin of existing users : '%s'", err)
os.Exit(1)
}
}
// Add IpAddress column to audit table
auditIPCheck, _ := CheckColumnExists("audit", "IpAddress")
if !auditIPCheck {
// Add the column for LdapGroup in the roles table
_, err := db.Exec("ALTER TABLE audit ADD COLUMN IpAddress VARCHAR;")
if err != nil {
log.Printf("Error altering audit table to add IpAddress column : '%s'\n", err)
os.Exit(1)
}
if _, err = db.Exec("UPDATE audit SET IpAddress = '' WHERE IpAddress is null;"); err != nil {
log.Printf("Error setting IpAddress of existing audit records : '%s'", err)
os.Exit(1)
}
}
// Set the schema version
rowCount, _ = CheckCount("schema")
if rowCount > 0 {
if _, err = db.Exec("UPDATE schema SET Version = 3;"); err != nil {
log.Printf("Error setting schema to version 3 : '%s'", err)
os.Exit(1)
}
} else {
if _, err = db.Exec("INSERT INTO schema (Version) VALUES (3);"); err != nil {
log.Printf("Error setting schema to version 3 : '%s'", err)
os.Exit(1)
}
}
}
}
// Count the number of records in the sqlite database
// Borrowed from https://gist.github.com/trkrameshkumar/f4f1c00ef5d578561c96?permalink_comment_id=2687592#gistcomment-2687592
func CheckCount(tablename string) (int, error) {
var count int
stmt, err := db.Prepare("SELECT COUNT(*) as count FROM " + tablename)
if err != nil {
log.Printf("CheckCount error preparing sqlite statement : '%s'\n", err)
return 0, err
}
err = stmt.QueryRow().Scan(&count)
if err != nil {
log.Printf("CheckCount error querying database record count : '%s'\n", err)
return 0, err
}
stmt.Close() // or use defer rows.Close(), idc
return count, nil
}
func GetSchemaVersion() (int, error) {
var version int
stmt, err := db.Prepare("SELECT Version FROM schema;")
if err != nil {
log.Printf("GetSchemaVersion error preparing sqlite statement : '%s'\n", err)
return 0, err
}
err = stmt.QueryRow().Scan(&version)
if err != nil {
log.Printf("GetSchemaVersion error querying database record count : '%s'\n", err)
return 0, err
}
stmt.Close() // or use defer rows.Close(), idc
return version, nil
}
// From https://stackoverflow.com/a/60100045
func GenerateInsertMethod(q interface{}) (string, error) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
query := fmt.Sprintf("INSERT INTO %s", reflect.TypeOf(q).Name())
fieldNames := ""
fieldValues := ""
v := reflect.ValueOf(q)
for i := 0; i < v.NumField(); i++ {
if i == 0 {
fieldNames = fmt.Sprintf("%s%s", fieldNames, v.Type().Field(i).Name)
} else {
fieldNames = fmt.Sprintf("%s, %s", fieldNames, v.Type().Field(i).Name)
}
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
fieldValues = fmt.Sprintf("%s%d", fieldValues, v.Field(i).Int())
} else {
fieldValues = fmt.Sprintf("%s, %d", fieldValues, v.Field(i).Int())
}
case reflect.String:
if i == 0 {
fieldValues = fmt.Sprintf("%s\"%s\"", fieldValues, v.Field(i).String())
} else {
fieldValues = fmt.Sprintf("%s, \"%s\"", fieldValues, v.Field(i).String())
}
case reflect.Bool:
var boolSet int8
if v.Field(i).Bool() {
boolSet = 1
}
if i == 0 {
fieldValues = fmt.Sprintf("%s%d", fieldValues, boolSet)
} else {
fieldValues = fmt.Sprintf("%s, %d", fieldValues, boolSet)
}
default:
log.Printf("Unsupported type '%s'\n", v.Field(i).Kind())
}
}
query = fmt.Sprintf("%s(%s) VALUES (%s)", query, fieldNames, fieldValues)
return query, nil
}
return "", errors.New("SqlGenerationError")
}
func CheckColumnExists(table string, column string) (bool, error) {
var count int64
rows, err := db.Queryx("SELECT COUNT(*) AS CNTREC FROM pragma_table_info('" + table + "') WHERE name='" + column + "';")
if err != nil {
log.Printf("CheckColumnExists error querying database for existence of column '%s' : '%s'\n", column, err)
return false, err
}
defer rows.Close()
for rows.Next() {
// cols is an []interface{} of all of the column results
cols, _ := rows.SliceScan()
log.Printf("CheckColumnExists Value is '%v' for table '%s' and column '%s'\n", cols[0].(int64), table, column)
count = cols[0].(int64)
if count == 1 {
return true, nil
} else {
return false, nil
}
}
err = rows.Err()
if err != nil {
log.Printf("CheckColumnExists error getting results : '%s'\n", err)
return false, err
}
return false, nil
}

143
models/group.go Normal file
View File

@@ -0,0 +1,143 @@
package models
import (
"errors"
"log"
)
type Group struct {
GroupId int `db:"GroupId" json:"groupId"`
GroupName string `db:"GroupName" json:"groupName"`
LdapGroup bool `db:"LdapGroup" json:"ldapGroup"`
LdapDn string `db:"LdapDn" json:"ldapDn"`
Admin bool `db:"Admin" json:"admin"`
}
// GroupGetByName queries the database for the specified group name
func GroupGetByName(groupname string) (Group, error) {
var g Group
// Query database for matching group object
err := db.QueryRowx("SELECT * FROM groups WHERE GroupName=?", groupname).StructScan(&g)
if err != nil {
return g, errors.New("group not found")
}
return g, nil
}
// GroupGetById queries the database for the specified group name
func GroupGetById(groupId int) (Group, error) {
var g Group
// Query database for matching group object
err := db.QueryRowx("SELECT * FROM groups WHERE GroupId=?", groupId).StructScan(&g)
if err != nil {
return g, errors.New("group not found")
}
return g, nil
}
// GroupGetByName queries the database for a group with the specified LDAP distinguishedName
func GroupGetByLdapDn(ldapDn string) (Group, error) {
var g Group
// Query database for matching group object
err := db.QueryRowx("SELECT * FROM groups WHERE LdapGroup = 1 AND LdapDn = ?", ldapDn).StructScan(&g)
if err != nil {
return g, errors.New("group not found")
}
return g, nil
}
// GroupList returns a list of all groups in database
func GroupList() ([]Group, error) {
var results []Group
// Query database for groups
rows, err := db.Queryx("SELECT * FROM groups")
if err != nil {
log.Printf("GroupList error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var g Group
err = rows.StructScan(&g)
if err != nil {
log.Printf("GroupList error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, g)
}
log.Printf("GroupList retrieved '%d' results\n", len(results))
}
return results, nil
}
// GroupAdd adds a new group definition to the database
func (g *Group) GroupAdd() (*Group, error) {
var err error
// Validate group not already in use
_, err = GroupGetByName(g.GroupName)
if err != nil && err.Error() == "group not found" {
log.Printf("GroupAdd confirmed no existing group, continuing with creation of group '%s'\n", g.GroupName)
result, err := db.NamedExec(("INSERT INTO groups (GroupName, LdapGroup, LdapDn, Admin) VALUES (:GroupName, :LdapGroup, :LdapDn, :Admin);"), g)
if err != nil {
log.Printf("GroupAdd error executing sql record : '%s'\n", err)
return &Group{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
g.GroupId = int(id)
log.Printf("GroupAdd insert returned result id '%d' affecting %d row(s).\n", id, affected)
}
} else {
errString := "group with name already exists"
log.Printf("GroupAdd %s\n", errString)
return &Group{}, errors.New(errString)
}
return g, nil
}
// GroupDelete removes a group definition from the database
func (g *Group) GroupDelete() error {
var err error
// Validate group exists
group, err := GroupGetByName(g.GroupName)
if err != nil && err.Error() == "group not found" {
log.Printf("GroupDelete unable to validate group exists : '%s'\n", err)
return err
}
// Make sure we have a group ID
if g.GroupId == 0 {
g.GroupId = group.GroupId
}
// Delete the group
log.Printf("GroupDelete confirmed group exists, continuing with deletion of group '%s'\n", g.GroupName)
result, err := db.NamedExec((`DELETE FROM groups WHERE GroupId = :GroupId`), g)
if err != nil {
log.Printf("GroupDelete error executing sql delete : '%s'\n", err)
return err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("GroupDelete returned result id '%d' affecting %d row(s).\n", id, affected)
}
return nil
}

119
models/key.go Normal file
View File

@@ -0,0 +1,119 @@
package models
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"smt/utils"
"golang.org/x/crypto/bcrypt"
)
const hashFileName = "verify.hash"
// TODO: Look at using shamir's secret sharing to distribute components of the secret key
var secretKey []byte
var secretReceived bool
func getHashFilePath() (string, error) {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return "", err
}
filePath := filepath.Join(dir, hashFileName)
return filePath, nil
}
func storeKeyHash(plaintext string, filePath string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintext), bcrypt.DefaultCost)
if err != nil {
log.Printf("storeKeyHash error generating hash : '%s'\n", err)
return err
}
err = os.WriteFile(filePath, hash, 0600)
if err != nil {
log.Printf("storeKeyHash error writing file : '%s'\n", err)
return err
}
log.Println("Bcrypt hash stored in file:", filePath)
return nil
}
func compareHashWithPlaintext(plaintext string, filePath string) (bool, error) {
hashBytes, err := os.ReadFile(filePath)
if err != nil {
log.Printf("compareHashWithPlaintext error reading hashfile : '%s'\n", err)
return false, err
}
err = bcrypt.CompareHashAndPassword(hashBytes, []byte(plaintext))
if err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
log.Printf("compareHashWithPlaintext provided key is incorrect")
return false, nil // Passwords don't match
}
log.Printf("compareHashWithPlaintext error comparing provided key : '%s'\n", err)
return false, err // Other error occurred
}
return true, nil // Passwords match
}
func ReceiveKey(key string) error {
// confirm that the key is 32 bytes long exactly
if len(key) != 32 {
return errors.New("secret key provided is not exactly 32 bytes long")
}
if os.Getenv("SECRETS_KEY") == "" {
// Hash the secret key and store it on disk so we can verify if correct secret key is received
filePath, _ := getHashFilePath()
if filePath != "" && utils.FileExists(filePath) {
log.Printf("ReceiveKey detected hash file at '%s'\n", filePath)
// File already exists, compare received key with hash in file
compare, err := compareHashWithPlaintext(key, filePath)
if err != nil {
return fmt.Errorf("unable to verify secret key: '%s'", err.Error())
}
if !compare {
return errors.New("secret key is not correct")
} else {
log.Printf("ReceiveKey successfully verified supplied key\n")
}
} else if filePath != "" {
log.Printf("ReceiveKey storing key into file '%s'\n", filePath)
storeKeyHash(key, filePath)
} else {
return fmt.Errorf("unable to determine path to key hash file '%s'", hashFileName)
}
} else {
log.Printf("ReceiveKey not storing hash on disk since we read key from environment variable")
}
// Store the secret key in memory so that we can access it when encrypting/decrypting
secretKey = []byte(key)
secretReceived = true
return nil
}
func ProvideKey() ([]byte, error) {
// Provide the key when needed to decrypt/encrypt stored secrets
if secretReceived {
return secretKey, nil
} else {
return nil, errors.New("secret key has not been received")
}
}
func CheckKeyProvided() bool {
return secretReceived
}

384
models/ldap.go Normal file
View File

@@ -0,0 +1,384 @@
package models
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/go-ldap/ldap/v3"
)
// Code relating to AD integration
type LdapConfig struct {
LdapBindAddress string
LdapBaseDn string
LdapCertFile string
}
var systemCA *x509.CertPool
// var ldaps *ldap.Conn
var LdapServer string
var CertLoaded bool
var LdapEnabled bool
var LdapInsecure bool = false
var LdapBaseDn string
var DefaultDomainSuffix string
func GetFilePath(path string) string {
// Check for empty filename
if len(path) == 0 {
return ""
}
// check if filename exists
if _, err := os.Stat(path); os.IsNotExist((err)) {
log.Printf("File '%s' not found, searching in same directory as binary\n", path)
// if not, check that it exists in the same directory as the currently executing binary
ex, err2 := os.Executable()
if err2 != nil {
//log.Printf("Error determining binary path : '%s'", err)
return ""
}
binaryPath := filepath.Dir(ex)
path = filepath.Join(binaryPath, path)
}
return path
}
// DomainSuffixFromNamingContext will convert DC=example,DC=com to example.com
func DomainSuffixFromNamingContext(input string) string {
tokens := strings.Split(input, ",")
var args []string
for _, token := range tokens {
parts := strings.Split(token, "=")
if len(parts) == 2 && parts[0] == "DC" {
args = append(args, parts[1])
}
}
return strings.Join(args, ".")
}
func CheckUsername(username string) string {
if strings.ContainsAny(username, "/@") {
// Username contains forward slash or at symbol
return username
}
// Append suffix to the username
log.Printf("CheckUsername appending default domain suffix '%s'\n", DefaultDomainSuffix)
return username + "@" + DefaultDomainSuffix
}
func loadLdapCert() {
var err error
// Get a copy of the system defined CA's
systemCA, err = x509.SystemCertPool()
if err != nil {
log.Printf("LoadLdapCert error getting system certificate pool : '%s'\n", err)
return
}
// only try to load certificate from file if the command line argument was specified
ldapCertFile := os.Getenv("LDAP_TRUST_CERT_FILE")
if ldapCertFile == "" {
log.Printf("LoadLdapCert no certificate specified\n")
return
} else {
// Try to read the file
cf, err := os.ReadFile(GetFilePath(ldapCertFile))
if err != nil {
log.Printf("LoadLdapCert error opening LDAP certificate file '%s' : '%s'\n", ldapCertFile, err)
return
}
// Get the certificate from the file
cpb, _ := pem.Decode(cf)
crt, err := x509.ParseCertificate(cpb.Bytes)
//log.Printf("Loaded certificate with subject %s\n", crt.Subject)
if err != nil {
log.Printf("LoadLdapCert error processing LDAP certificate file '%s' : '%s'\n", ldapCertFile, err)
return
}
// Add custom certificate to the system cert pool
systemCA.AddCert(crt)
CertLoaded = true
}
}
func LdapSetup() bool {
var err error
// Load LDAP certificate if necessary
loadLdapCert()
LdapServer = os.Getenv("LDAP_BIND_ADDRESS")
if LdapServer == "" {
log.Printf("VerifyLdapCreds no LDAP bind address supplied\n")
return false
} else {
LdapEnabled = true
}
LdapBaseDn = os.Getenv("LDAP_BASE_DN")
if LdapBaseDn == "" {
log.Printf("VerifyLdapCreds no LDAP base DN supplied\n")
return false
}
insecure := os.Getenv("LDAP_INSECURE_VALIDATION")
if insecure != "" {
LdapInsecure, err = strconv.ParseBool(insecure)
if err != nil {
log.Printf("LdapSetup could not convert environment variable LDAP_INSECURE_VALIDATION with value of '%s'\n", insecure)
}
}
// Set up TLS to use our custom certificate authority passed in cli argument
tlsConfig := &tls.Config{
RootCAs: systemCA,
InsecureSkipVerify: true,
}
// Add port if not specified in .env file
if !(strings.HasSuffix(LdapServer, ":636")) {
LdapServer = fmt.Sprintf("%s:636", LdapServer)
log.Printf("VerifyLdapCreds updated ldapServer string '%s'\n", LdapServer)
}
// try connecting to AD via TLS and our custom certificate authority
ldaps, err := ldap.DialTLS("tcp", LdapServer, tlsConfig)
if err != nil {
log.Printf("VerifyLdapCreds error connecting to LDAP bind address '%s' : '%s'\n", LdapServer, err)
return false
}
LdapEnabled = true
namingContext := LookupNamingContext(ldaps)
if namingContext != "" {
DefaultDomainSuffix = DomainSuffixFromNamingContext(namingContext)
}
ldaps.Close()
return true
}
// LdapConnect sets up the connection to LDAP to be used by other functions
func ldapConnect() *ldap.Conn {
// Set up TLS to use our custom certificate authority passed in cli argument
tlsConfig := &tls.Config{
RootCAs: systemCA,
InsecureSkipVerify: LdapInsecure,
}
log.Printf("ldapConnect initiating connection\n")
ldaps, err := ldap.DialTLS("tcp", LdapServer, tlsConfig)
if err != nil {
log.Printf("VerifyLdapCreds error connecting to LDAP server '%s' : '%s'\n", LdapServer, err)
return nil
}
log.Printf("ldapConnect connection succeeded\n")
return ldaps
}
func LookupNamingContext(ldaps *ldap.Conn) string {
// Retrieve the defaultNamingContext
searchRequest := ldap.NewSearchRequest(
"",
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=*)",
[]string{"defaultNamingContext"},
nil,
)
searchResult, err := ldaps.Search(searchRequest)
if err != nil {
log.Printf("LookupNamingContext unable to perform unauthenticated search : '%s'\n", err)
return ""
}
if len(searchResult.Entries) != 1 {
log.Printf("LookupNamingContext unable to retrieve defaultNamingContext\n")
return ""
}
defaultNamingContext := searchResult.Entries[0].GetAttributeValue("defaultNamingContext")
if defaultNamingContext == "" {
log.Printf("LookupNamingContext defaultNamingContext attribute not found\n")
return ""
}
log.Printf("Default Naming Context: '%s'\n", defaultNamingContext)
return defaultNamingContext
}
// LdapGetGroupMembership returns a list of distinguishedNames for groups that a user is a member of
func LdapGetGroupMembership(username string, password string) ([]string, error) {
var err error
username = CheckUsername(username)
ldaps := ldapConnect()
defer ldaps.Close()
// try an authenticated bind to AD to verify credentials
log.Printf("LdapGetGroupMembership Attempting LDAP bind with user '%s' and password length '%d'\n", username, len(password))
err = ldaps.Bind(username, password)
if err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
errString := "invalid user credentials"
log.Print(errString)
return nil, errors.New(errString)
} else {
errString := fmt.Sprintf("LdapGetGroupMembership error binding to LDAP with supplied credentials : '%s'\n", err)
log.Print(errString)
return nil, errors.New(errString)
}
} else {
log.Printf("LdapGetGroupMembership successfully bound to LDAP\n")
}
groups, err := GetGroupsOfUser(username, LdapBaseDn, ldaps)
if err != nil {
errString := fmt.Sprintf("LdapGetGroupMembership group search error : '%s'\n", err)
log.Print(errString)
return nil, errors.New(errString)
}
return groups, nil
}
// VerifyLdapCreds validates that we can bind successfully to LDAP with the supplied credentials
func VerifyLdapCreds(username string, password string) error {
var err error
username = CheckUsername(username)
ldaps := ldapConnect()
defer ldaps.Close()
// try an authenticated bind to AD to verify credentials
log.Printf("Attempting LDAP bind with user '%s' and password length '%d'\n", username, len(password))
err = ldaps.Bind(username, password)
if err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
errString := "invalid user credentials"
log.Print(errString)
return errors.New(errString)
} else {
errString := fmt.Sprintf("VerifyLdapCreds error binding to LDAP with supplied credentials : '%s'\n", err)
log.Print(errString)
return errors.New(errString)
}
} else {
log.Printf("VerifyLdapCreds successfully bound to LDAP\n")
}
return nil
}
// GetGroupsOfUser returns the group for a user.
// Taken from https://github.com/jtblin/go-ldap-client/issues/13#issuecomment-456090979
func GetGroupsOfUser(username string, baseDN string, conn *ldap.Conn) ([]string, error) {
var sAMAccountName string
var groups []string
if strings.Contains(username, "@") {
s := strings.Split(username, "@")
sAMAccountName = s[0]
} else if strings.Contains(username, "\\") {
s := strings.Split(username, "\\")
sAMAccountName = s[len(s)-1]
} else {
sAMAccountName = username
}
// Get the users DN
// Search for the given username
searchRequest := ldap.NewSearchRequest(
baseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(sAMAccountName=%s)", ldap.EscapeFilter(sAMAccountName)),
[]string{},
nil,
)
log.Printf("searchRequest: %v\n", searchRequest)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, err
}
if len(sr.Entries) != 1 {
return nil, fmt.Errorf("user '%s' does not exist", sAMAccountName)
} else {
// Get the groups of the first result
groups = sr.Entries[0].GetAttributeValues("memberOf")
}
return groups, nil
}
func GetLdapUserDn(username string, baseDN string, conn *ldap.Conn) (string, error) {
var sAMAccountName string
if strings.Contains(username, "@") {
s := strings.Split(username, "@")
sAMAccountName = s[0]
} else if strings.Contains(username, "\\") {
s := strings.Split(username, "\\")
sAMAccountName = s[len(s)-1]
} else {
sAMAccountName = username
}
// Search for the user's distinguishedName
searchRequest := ldap.NewSearchRequest(
baseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(sAMAccountName=%s)", sAMAccountName),
[]string{"distinguishedName"},
nil,
)
searchResult, err := conn.Search(searchRequest)
if err != nil {
log.Fatal(err)
}
if len(searchResult.Entries) == 0 {
return "", fmt.Errorf("user '%s' does not exist", sAMAccountName)
} else {
// Retrieve the distinguishedName of the user
distinguishedName := searchResult.Entries[0].GetAttributeValue("distinguishedName")
if distinguishedName != "" {
log.Printf("GetLdapUserDn located user's distinguishedName : '%s'\n", distinguishedName)
return distinguishedName, nil
} else {
return "", fmt.Errorf("could not find distinguishedName for user '%s'", sAMAccountName)
}
}
}
// Returns the user portion of a UPN formatted username
func GetUserFromUPN(email string) string {
parts := strings.Split(email, "@")
if len(parts) > 0 {
return parts[0]
}
return ""
}

170
models/permission.go Normal file
View File

@@ -0,0 +1,170 @@
package models
import (
"errors"
"log"
)
type Permission struct {
PermissionId int `db:"PermissionId" json:"permissionId"`
Description string `db:"Description" json:"description"`
ReadOnly bool `db:"ReadOnly" json:"readOnly"`
SafeId int `db:"SafeId" json:"safeId"`
UserId int `db:"UserId" json:"userId"`
GroupId int `db:"GroupId" json:"groupId"`
}
// PermissionGetByDesc queries the database for a permission record matching the specified description
func PermissionGetByDesc(description string) (Permission, error) {
var p Permission
// Query database for matching group object
err := db.QueryRowx("SELECT * FROM permissions WHERE Description=?", description).StructScan(&p)
if err != nil {
return p, errors.New("permission not found")
}
return p, nil
}
// PermissionGetById queries the database for a permission record matching the specified permission id
func PermissionGetById(id int) (Permission, error) {
var p Permission
// Query database for matching group object
err := db.QueryRowx("SELECT * FROM permissions WHERE PermissionId=?", id).StructScan(&p)
if err != nil {
return p, errors.New("permission not found")
}
return p, nil
}
// PermissionList returns a list of all permissions in database
func PermissionList() ([]Permission, error) {
var results []Permission
// Query database for groups
rows, err := db.Queryx("SELECT * FROM permissions")
if err != nil {
log.Printf("PermissionList error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var p Permission
err = rows.StructScan(&p)
if err != nil {
log.Printf("PermissionList error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, p)
}
log.Printf("PermissionList retrieved '%d' results\n", len(results))
}
return results, nil
}
// PermissionAdd adds a new permission definition to the database
func (p *Permission) PermissionAdd() (*Permission, error) {
var err error
//var check Permission
if len(p.Description) > 0 {
_, err = PermissionGetByDesc(p.Description)
} else {
return &Permission{}, errors.New("unable to identify permission with supplied parameters")
}
if err != nil && err.Error() == "permission not found" {
log.Printf("PermissionAdd confirmed no existing permission, continuing with creation of permission '%s'\n", p.Description)
result, err := db.NamedExec(("INSERT INTO permissions (Description, SafeId, UserId, GroupId, ReadOnly) VALUES (:Description, :SafeId, :UserId, :GroupId, :ReadOnly);"), p)
if err != nil {
log.Printf("PermissionAdd error executing sql record : '%s'\n", err)
return &Permission{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
p.PermissionId = int(id)
log.Printf("PermissionAdd insert returned result id '%d' affecting %d row(s).\n", id, affected)
}
} else {
errString := "permission with identical description already exists"
log.Printf("PermissionAdd %s\n", errString)
return &Permission{}, errors.New(errString)
}
return p, nil
}
// PermissionDelete removes a permission definition from the database
func (p *Permission) PermissionDelete() error {
var err error
var permission Permission
// Validate permission exists
if p.PermissionId > 0 {
permission, err = PermissionGetById(p.PermissionId)
} else if len(p.Description) > 0 {
permission, err = PermissionGetByDesc(p.Description)
} else {
errString := "unable to identify permission with supplied parameters"
log.Printf("PermissionDelete %s\n", errString)
return errors.New(errString)
}
if err != nil && err.Error() == "permission not found" {
log.Printf("PermissionDelete unable to validate group exists : '%s'\n", err)
return err
}
// Make sure we have a group ID
if p.PermissionId == 0 {
p.PermissionId = permission.PermissionId
}
// Delete the group
log.Printf("PermissionDelete confirmed group exists, continuing with deletion of permission id %d, '%s'\n", p.PermissionId, p.Description)
result, err := db.NamedExec((`DELETE FROM permissions WHERE PermissionId = :PermissionId`), p)
if err != nil {
log.Printf("PermissionDelete error executing sql delete : '%s'\n", err)
return err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("PermissionDelete returned result id '%d' affecting %d row(s).\n", id, affected)
}
return nil
}
// PermissionUpdate updates an existing permission definition in the database
func (p *Permission) PermissionUpdate() (*Permission, error) {
var err error
log.Printf("PermissionUpdate storing values '%v'\n", p)
if p.PermissionId == 0 {
err = errors.New("PermissionUpdate unable to update permission with empty PermissionId field")
log.Printf("PermissionUpdate error in pre-check : '%s'\n", err)
return p, err
}
result, err := db.NamedExec((`UPDATE permissions SET Description = :Description, ReadOnly = :ReadOnly, SafeId = :SafeId, UserId = :UserId, GroupId = :GroupId WHERE PermissionId = :PermissionId`), p)
if err != nil {
log.Printf("PermissionUpdate error executing sql record : '%s'\n", err)
return &Permission{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("PermissionUpdate returned result id '%d' affecting %d row(s).\n", id, affected)
}
return p, nil
}

View File

@@ -1,37 +0,0 @@
package models
import "log"
type Role struct {
RoleId int `db:"RoleId"`
RoleName string `db:"RoleName"`
ReadOnly bool `db:"ReadOnly"`
Admin bool `db:"Admin"`
}
func QueryRoles() ([]Role, error) {
var results []Role
// Query database for role definitions
rows, err := db.Queryx("SELECT * FROM roles")
if err != nil {
log.Printf("QueryRoles error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var r Role
err = rows.StructScan(&r)
if err != nil {
log.Printf("QueryRoles error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, r)
}
log.Printf("QueryRoles retrieved '%d' results\n", len(results))
}
return results, nil
}

83
models/role.go.txt Normal file
View File

@@ -0,0 +1,83 @@
package models
import (
"errors"
"log"
)
type Role struct {
RoleId int `db:"RoleId"`
RoleName string `db:"RoleName"`
LdapGroup string `db:"LdapGroup"`
ReadOnly bool `db:"ReadOnly"`
Admin bool `db:"Admin"`
}
func QueryRoles() ([]Role, error) {
var results []Role
// Query database for role definitions
rows, err := db.Queryx("SELECT * FROM roles")
if err != nil {
log.Printf("QueryRoles error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var r Role
err = rows.StructScan(&r)
if err != nil {
log.Printf("QueryRoles error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, r)
}
log.Printf("QueryRoles retrieved '%d' results\n", len(results))
}
return results, nil
}
// AddRole adds a new role definition to the database
func (r *Role) AddRole() (*Role, error) {
var err error
// Validate role not already in use
_, err = GetRoleByName(r.RoleName)
// TODO
if err != nil && err.Error() == "role not found" {
log.Printf("AddRole confirmed no existing role, continuing with creation of role '%s'\n", r.RoleName)
result, err := db.NamedExec(("INSERT INTO roles (RoleName, ReadOnly, Admin, LdapGroup) VALUES (:RoleName, :ReadOnly, :Admin, :LdapGroup);"), r)
if err != nil {
log.Printf("AddRole error executing sql record : '%s'\n", err)
return &Role{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("AddRole insert returned result id '%d' affecting %d row(s).\n", id, affected)
}
} else {
log.Printf("AddRole RoleName already exists : '%v'\n", err)
}
return r, nil
}
func GetRoleByName(rolename string) (Role, error) {
var r Role
// Query database for matching user object
err := db.QueryRowx("SELECT * FROM roles WHERE RoleName=?", rolename).StructScan(&r)
if err != nil {
return r, errors.New("role not found")
}
return r, nil
}

152
models/safe.go Normal file
View File

@@ -0,0 +1,152 @@
package models
import (
"errors"
"log"
"smt/utils"
)
type Safe struct {
SafeId int `db:"SafeId" json:"safeId"`
SafeName string `db:"SafeName" json:"safeName"`
}
// SafeGetByName queries the database for the specified safe name
func SafeGetByName(safename string) (Safe, error) {
var s Safe
// Query database for matching group object
err := db.QueryRowx("SELECT * FROM safes WHERE SafeName=?", safename).StructScan(&s)
if err != nil {
return s, errors.New("safe not found")
}
return s, nil
}
// SafeList returns a list of all safes in database
func SafeList() ([]Safe, error) {
var results []Safe
// Query database for safes
rows, err := db.Queryx("SELECT * FROM safes")
if err != nil {
log.Printf("SafeList error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var s Safe
err = rows.StructScan(&s)
if err != nil {
log.Printf("SafeList error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, s)
}
log.Printf("SafeList retrieved '%d' results\n", len(results))
}
return results, nil
}
// SafeList returns a list of safes in database that a user has access to
func SafeListAllowed(userId int) ([]Safe, error) {
var results []Safe
// Query database for safes
rows, err := db.Queryx(`
SELECT safes.* FROM safes INNER JOIN permissions ON safes.SafeId = permissions.SafeId INNER JOIN users ON users.GroupId = permissions.GroupId WHERE users.UserId = ?
UNION
SELECT safes.* FROM safes INNER JOIN permissions ON safes.SafeId = permissions.SafeId INNER JOIN users ON users.UserId = permissions.UserId WHERE users.UserId = ?
`, userId, userId)
if err != nil {
log.Printf("SafeListAllowed error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var s Safe
err = rows.StructScan(&s)
if err != nil {
log.Printf("SafeListAllowed error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, s)
debugPrint := utils.PrintStructContents(&s, 0)
log.Printf("SafeListAllowed adding record :\n%s\n", debugPrint)
}
log.Printf("SafeListAllowed retrieved '%d' results\n", len(results))
}
return results, nil
}
// SafeAdd adds a new safe definition to the database
func (s *Safe) SafeAdd() (*Safe, error) {
var err error
// Validate group not already in use
_, err = SafeGetByName(s.SafeName)
if err != nil && err.Error() == "safe not found" {
log.Printf("SafeAdd confirmed no existing safe, continuing with creation of safe '%s'\n", s.SafeName)
result, err := db.NamedExec(("INSERT INTO safes (SafeName) VALUES (:SafeName) RETURNING SafeId;"), s)
//err = db.QueryRowx(`INSERT INTO users (user_id, user_nme, user_email, user_address_id) VALUES ($1, $2, $3, $4) RETURNING *;`, 6, fake.UserName(), fake.EmailAddress(), lastInsertId).StructScan(&user)
if err != nil {
log.Printf("SafeAdd error executing sql record : '%s'\n", err)
return &Safe{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("SafeAdd insert returned result id '%d' affecting %d row(s).\n", id, affected)
s.SafeId = int(id)
log.Printf("safe: %v\n", s)
}
} else {
errString := "safe with name already exists"
log.Printf("SafeAdd %s\n", errString)
return &Safe{}, errors.New(errString)
}
return s, nil
}
// SafeDelete removes a safe definition from the database
func (s *Safe) SafeDelete() error {
var err error
// Validate group exists
safe, err := SafeGetByName(s.SafeName)
if err != nil && err.Error() == "safe not found" {
log.Printf("SafeDelete unable to validate safe exists : '%s'\n", err)
return err
}
// Make sure we have a safe ID
if s.SafeId == 0 {
s.SafeId = safe.SafeId
}
// Delete the safe
log.Printf("SafeDelete confirmed safe exists, continuing with deletion of safe '%s'\n", s.SafeName)
result, err := db.NamedExec((`DELETE FROM safes WHERE SafeId = :SafeId`), s)
if err != nil {
log.Printf("SafeDelete error executing sql delete : '%s'\n", err)
return err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("SafeDelete returned result id '%d' affecting %d row(s).\n", id, affected)
}
return nil
}

View File

@@ -6,68 +6,241 @@ import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"os"
"github.com/jmoiron/sqlx"
"smt/utils"
"strings"
"time"
)
// We use the json:"-" field tag to prevent showing these details to the user
type Secret struct {
SecretId int `db:"SecretId" json:"-"`
RoleId int `db:"RoleId" json:"-"`
DeviceName string `db:"DeviceName"`
DeviceCategory string `db:"DeviceCategory"`
UserName string `db:"UserName"`
Secret string `db:"Secret"`
}
const nonceSize = 12
func (s *Secret) SaveSecret() (*Secret, error) {
// We use the json:"-" field tag to prevent showing these details to the user
type Secret struct {
SecretId int `db:"SecretId" json:"secretId"`
SafeId int `db:"SafeId" json:"safeId"`
DeviceName string `db:"DeviceName" json:"deviceName"`
DeviceCategory string `db:"DeviceCategory" json:"deviceCategory"`
UserName string `db:"UserName" json:"userName"`
Secret string `db:"Secret" json:"secret"`
LastUpdated time.Time `db:"LastUpdated" json:"lastUpdated"`
}
// SecretRestricted is for when we want to output a Secret but not the protected information
type SecretRestricted struct {
SecretId int `db:"SecretId" json:"secretId"`
SafeId int `db:"SafeId" json:"safeId"`
DeviceName string `db:"DeviceName" json:"deviceName"`
DeviceCategory string `db:"DeviceCategory" json:"deviceCategory"`
UserName string `db:"UserName" json:"userName"`
Secret string `db:"Secret" json:"-"`
LastUpdated time.Time `db:"LastUpdated" json:"lastUpdated"`
}
// Used for querying all secrets the user has access to
// Since there are some ambiguous column names (eg UserName is present in both users and secrets table), the order of fields in this struct matters
type UserSecret struct {
Secret
UserUserId int `db:"UserUserId"`
UserUserName string `db:"UserUserName"`
User
//Group
Permission
}
// This method allows us to use an interface to avoid adding duplicate entries to a []Secret
func (s Secret) GetId() int {
return s.SecretId
}
func (s *Secret) SaveSecret() (*Secret, error) {
var err error
// Populate timestamp field if not already set
if s.LastUpdated.IsZero() {
s.LastUpdated = time.Now().UTC()
}
log.Printf("SaveSecret storing values '%v'\n", s)
result, err := db.NamedExec((`INSERT INTO secrets (RoleId, DeviceName, DeviceCategory, UserName, Secret) VALUES (:RoleId, :DeviceName, :DeviceCategory, :UserName, :Secret)`), s)
result, err := db.NamedExec((`INSERT INTO secrets (SafeId, DeviceName, DeviceCategory, UserName, Secret, LastUpdated) VALUES (:SafeId, :DeviceName, :DeviceCategory, :UserName, :Secret, :LastUpdated)`), s)
if err != nil {
log.Printf("StoreSecret error executing sql record : '%s'\n", err)
return s, err
} else {
}
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
s.SecretId = int(id)
log.Printf("StoreSecret insert returned result id '%d' affecting %d row(s).\n", id, affected)
}
return s, nil
}
// Returns all matching secrets, up to caller to determine how to deal with multiple results
func GetSecrets(s *Secret) ([]Secret, error) {
// SecretsGetAllowed returns all allowed secrets matching the specified parameters in s
func SecretsGetAllowed(s *Secret, userId int) ([]UserSecret, error) {
var err error
var rows *sqlx.Rows
var secretResults []Secret
var secretResults []UserSecret
log.Printf("GetSecret querying values '%v'\n", s)
// Query for group access
queryArgs := []interface{}{}
query := `
SELECT users.UserId AS UserUserId, users.UserName AS UserUserName, permissions.*,
secrets.SecretId, secrets.SafeId, secrets.DeviceName, secrets.DeviceCategory, secrets.UserName
FROM users
INNER JOIN groups ON users.GroupId = groups.GroupId
INNER JOIN permissions ON groups.GroupId = permissions.GroupId
INNER JOIN secrets on secrets.SafeId = permissions.SafeId
WHERE users.UserId = ? `
queryArgs = append(queryArgs, userId)
// Determine whether to query for a specific device or a category of devices
// Prefer querying device name than category
if s.DeviceName != "" && s.DeviceCategory != "" {
rows, err = db.Queryx("SELECT * FROM secrets WHERE DeviceName LIKE ? AND DeviceCategory LIKE ? AND RoleId = ?", s.DeviceName, s.DeviceCategory, s.RoleId)
} else if s.DeviceName != "" {
rows, err = db.Queryx("SELECT * FROM secrets WHERE DeviceName LIKE ? AND RoleId = ?", s.DeviceName, s.RoleId)
} else if s.DeviceCategory != "" {
rows, err = db.Queryx("SELECT * FROM secrets WHERE DeviceCategory LIKE ? AND RoleId = ?", s.DeviceCategory, s.RoleId)
} else {
log.Printf("GetSecret no valid search options specified\n")
err = errors.New("no valid search options specified")
return secretResults, err
// Add any other arguments to the query if they were specified
if s.SecretId > 0 {
query += " AND SecretId = ? "
queryArgs = append(queryArgs, s.SecretId)
}
if s.DeviceName != "" {
query += " AND DeviceName LIKE ? "
queryArgs = append(queryArgs, s.DeviceName)
}
if s.DeviceCategory != "" {
query += " AND DeviceCategory LIKE ? "
queryArgs = append(queryArgs, s.DeviceCategory)
}
if s.UserName != "" {
query += " AND secrets.UserName LIKE ? "
queryArgs = append(queryArgs, s.UserName)
}
// Query for user access
query += `
UNION
SELECT users.UserId AS UserUserId, users.UserName AS UserUserName, permissions.*,
secrets.SecretId, secrets.SafeId, secrets.DeviceName, secrets.DeviceCategory, secrets.UserName
FROM users
INNER JOIN permissions ON users.UserId = permissions.UserId
INNER JOIN safes on permissions.SafeId = safes.SafeId
INNER JOIN secrets on secrets.SafeId = safes.SafeId
WHERE users.UserId = ?`
queryArgs = append(queryArgs, userId)
// Add any other arguments to the query if they were specified
if s.SecretId > 0 {
query += " AND SecretId = ? "
queryArgs = append(queryArgs, s.SecretId)
}
if s.DeviceName != "" {
query += " AND DeviceName LIKE ? "
queryArgs = append(queryArgs, s.DeviceName)
}
if s.DeviceCategory != "" {
query += " AND DeviceCategory LIKE ? "
queryArgs = append(queryArgs, s.DeviceCategory)
}
if s.UserName != "" {
query += " AND secrets.UserName LIKE ? "
queryArgs = append(queryArgs, s.UserName)
}
// Execute the query
//log.Printf("SecretsGetAllowed query string : '%s'\nArguments:%+v\n", query, queryArgs)
rows, err := db.Queryx(query, queryArgs...)
if err != nil {
log.Printf("GetSecret error executing sql record : '%s'\n", err)
log.Printf("SecretsGetAllowed error executing sql record : '%s'\n", err)
return secretResults, err
} else {
//log.Printf("SecretsGetAllowed any error '%s'\n", rows.Err())
// parse all the results into a slice
for rows.Next() {
//log.Printf("SecretsGetAllowed processing row\n")
var r UserSecret
err = rows.StructScan(&r)
//log.Printf("SecretsGetAllowed performed struct scan\n")
if err != nil {
log.Printf("SecretsGetAllowed error parsing sql record : '%s'\n", err)
return secretResults, err
}
//log.Printf("r: %v\n", r)
//log.Printf("SecretsGetAllowed performed err check\n")
// work around to get the UserId populated in the User field of the struct
r.User.UserId = r.UserUserId
r.User.UserName = r.UserUserName
// For debugging purposes
//debugPrint := utils.PrintStructContents(&r, 0)
//log.Println(debugPrint)
// Use generics and the GetID() method on the UserSecret struct
// to avoid adding this element to the results
// if there is already a secret with the same ID present
secretResults = utils.AppendIfNotExists(secretResults, r)
//log.Printf("SecretsGetAllowed added secret results\n")
}
log.Printf("SecretsGetAllowed retrieved '%d' results\n", len(secretResults))
}
return secretResults, nil
}
// SecretsGetFromMultipleSafes queries the specified safes for matching secrets
func SecretsGetFromMultipleSafes(s *Secret, safeIds []int) ([]Secret, error) {
var err error
var secretResults []Secret
queryArgs := []interface{}{}
var query string
// Generate placeholders for the IN clause to match multiple SafeId values
placeholders := make([]string, len(safeIds))
for i := range safeIds {
placeholders[i] = "?"
}
placeholderStr := strings.Join(placeholders, ",")
// Create query with the necessary placeholders
query = fmt.Sprintf("SELECT * FROM secrets WHERE SafeId IN (%s) ", placeholderStr)
// Add the Safe Ids to the arguments list
for _, g := range safeIds {
queryArgs = append(queryArgs, g)
}
// Add any other arguments to the query if they were specified
if s.SecretId > 0 {
query += " AND SecretId = ? "
queryArgs = append(queryArgs, s.SecretId)
}
if s.DeviceName != "" {
query += " AND DeviceName LIKE ? "
queryArgs = append(queryArgs, s.DeviceName)
}
if s.DeviceCategory != "" {
query += " AND DeviceCategory LIKE ? "
queryArgs = append(queryArgs, s.DeviceCategory)
}
if s.UserName != "" {
query += " AND UserName LIKE ? "
queryArgs = append(queryArgs, s.UserName)
}
// Execute the query
//log.Printf("SecretsGetMultipleSafes query string :\n'%s'\nQuery Args : %+v\n", query, queryArgs)
rows, err := db.Queryx(query, queryArgs...)
if err != nil {
log.Printf("SecretsGetMultipleSafes error executing sql record : '%s'\n", err)
return secretResults, err
} else {
// parse all the results into a slice
@@ -75,20 +248,22 @@ func GetSecrets(s *Secret) ([]Secret, error) {
var r Secret
err = rows.StructScan(&r)
if err != nil {
log.Printf("GetSecret error parsing sql record : '%s'\n", err)
log.Printf("SecretsGetMultipleSafes error parsing sql record : '%s'\n", err)
return secretResults, err
}
// Decrypt the secret
_, err = r.DecryptSecret()
if err != nil {
log.Printf("GetSecret unable to decrypt stored secret '%v', skipping result.\n", r.Secret)
log.Printf("SecretsGetMultipleSafes unable to decrypt stored secret : '%s'\n", err)
rows.Close()
return secretResults, err
} else {
secretResults = append(secretResults, r)
}
}
log.Printf("GetSecret retrieved '%d' results\n", len(secretResults))
log.Printf("SecretsGetMultipleSafes retrieved '%d' results\n", len(secretResults))
}
return secretResults, nil
@@ -98,6 +273,11 @@ func (s *Secret) UpdateSecret() (*Secret, error) {
var err error
// Populate timestamp field if not already set
if s.LastUpdated.IsZero() {
s.LastUpdated = time.Now().UTC()
}
log.Printf("UpdateSecret storing values '%v'\n", s)
if s.SecretId == 0 {
@@ -106,7 +286,7 @@ func (s *Secret) UpdateSecret() (*Secret, error) {
return s, err
}
result, err := db.NamedExec((`UPDATE secrets SET DeviceName = :DeviceName, DeviceCategory = :DeviceCategory, UserName = :UserName, Secret = :Secret WHERE SecretId = :SecretId`), s)
result, err := db.NamedExec((`UPDATE secrets SET DeviceName = :DeviceName, DeviceCategory = :DeviceCategory, UserName = :UserName, Secret = :Secret, LastUpdated = :LastUpdated WHERE SecretId = :SecretId`), s)
if err != nil {
log.Printf("UpdateSecret error executing sql record : '%s'\n", err)
return &Secret{}, err
@@ -119,22 +299,92 @@ func (s *Secret) UpdateSecret() (*Secret, error) {
return s, nil
}
func (s *Secret) EncryptSecret() (*Secret, error) {
func (s *Secret) DeleteSecret() (*Secret, error) {
keyString := os.Getenv("SECRETS_KEY")
// The key argument should be the AES key, either 16 or 32 bytes
// to select AES-128 or AES-256.
key := []byte(keyString)
plaintext := []byte(s.Secret)
var err error
//log.Printf("EncryptSecret applying key '%v' of length '%d' to plaintext secret '%s'\n", key, len(key), s.Secret)
log.Printf("DeleteSecret deleting record with values '%v'\n", s)
if s.SecretId == 0 {
err = errors.New("unable to locate secret with empty secretId field")
log.Printf("DeleteSecret error in pre-check : '%s'\n", err)
return s, err
}
result, err := db.NamedExec((`DELETE FROM secrets WHERE SecretId = :SecretId`), s)
if err != nil {
log.Printf("DeleteSecret error executing sql record : '%s'\n", err)
return &Secret{}, err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("DeleteSecret delete returned result id '%d' affecting %d row(s).\n", id, affected)
}
return s, nil
}
// startCipher does the initial setup of the AES256 GCM mode cipher
func startCipher() (cipher.AEAD, error) {
key, err := ProvideKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
log.Printf("startCipher NewCipher error '%s'\n", err)
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
log.Printf("startCipher NewGCM error '%s'\n", err)
return nil, err
}
return aesgcm, nil
}
func (s *Secret) EncryptSecret() (*Secret, error) {
//keyString := os.Getenv("SECRETS_KEY")
//keyString := secretKey
// The key argument should be the AES key, either 16 or 32 bytes
// to select AES-128 or AES-256.
//key := []byte(keyString)
/*
key, err := ProvideKey()
if err != nil {
return s, err
}
*/
plaintext := []byte(s.Secret)
// TODO : move block and aesgcm generation to separate function since the identical code is used for encrypt and decrypt
/*
log.Printf("EncryptSecret applying key '%v' of length '%d' to plaintext secret '%s'\n", key, len(key), s.Secret)
block, err := aes.NewCipher(key)
if err != nil {
log.Printf("EncryptSecret NewCipher error '%s'\n", err)
return s, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
log.Printf("EncryptSecret NewGCM error '%s'\n", err)
return s, err
}
*/
aesgcm, err := startCipher()
if err != nil {
log.Printf("EncryptSecret error commencing GCM cipher '%s'\n", err)
return s, err
}
// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
nonce := make([]byte, nonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
@@ -143,12 +393,6 @@ func (s *Secret) EncryptSecret() (*Secret, error) {
}
//log.Printf("EncryptSecret random nonce value is '%x'\n", nonce)
aesgcm, err := cipher.NewGCM(block)
if err != nil {
log.Printf("EncryptSecret NewGCM error '%s'\n", err)
return s, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
//log.Printf("EncryptSecret generated ciphertext '%x''\n", ciphertext)
@@ -168,8 +412,16 @@ func (s *Secret) EncryptSecret() (*Secret, error) {
func (s *Secret) DecryptSecret() (*Secret, error) {
// The key argument should be the AES key, either 16 or 32 bytes
// to select AES-128 or AES-256.
keyString := os.Getenv("SECRETS_KEY")
key := []byte(keyString)
//keyString := os.Getenv("SECRETS_KEY")
//keyString := secretKey
//key := []byte(keyString)
/*
key, err := ProvideKey()
if err != nil {
return s, err
}
*/
if len(s.Secret) < nonceSize {
log.Printf("DecryptSecret ciphertext is too short to decrypt\n")
@@ -188,7 +440,8 @@ func (s *Secret) DecryptSecret() (*Secret, error) {
nonce := crypted[:nonceSize]
ciphertext := crypted[nonceSize:]
//log.Printf("DecryptSecret applying key '%v' and nonce '%x' to ciphertext '%x'\n", key, nonce, ciphertext)
/*
log.Printf("DecryptSecret applying key '%v' and nonce '%x' to ciphertext '%x'\n", key, nonce, ciphertext)
block, err := aes.NewCipher(key)
if err != nil {
@@ -201,6 +454,13 @@ func (s *Secret) DecryptSecret() (*Secret, error) {
log.Printf("DecryptSecret NewGCM error '%s'\n", err)
return s, err
}
*/
aesgcm, err := startCipher()
if err != nil {
log.Printf("DecryptSecret error commencing GCM cipher '%s'\n", err)
return s, err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {

View File

@@ -1,246 +0,0 @@
package models
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"ccsecrets/utils"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
var db *sqlx.DB
const (
sqlFile = "smt.db"
)
const createRoles string = `
CREATE TABLE IF NOT EXISTS roles (
RoleId INTEGER PRIMARY KEY ASC,
RoleName VARCHAR,
ReadOnly BOOLEAN,
Admin BOOLEAN
);
`
const createUsers string = `
CREATE TABLE IF NOT EXISTS users (
UserId INTEGER PRIMARY KEY ASC,
RoleId INTEGER,
UserName VARCHAR,
Password VARCHAR,
FOREIGN KEY (RoleId) REFERENCES roles(RoleId)
);
`
const createSecrets string = `
CREATE TABLE IF NOT EXISTS secrets (
SecretId INTEGER PRIMARY KEY ASC,
RoleId INTEGER,
DeviceName VARCHAR,
DeviceCategory VARCHAR,
UserName VARCHAR,
Secret VARCHAR,
FOREIGN KEY (RoleId) REFERENCES roles(RoleId)
);
`
const createSchema string = `
CREATE TABLE IF NOT EXISTS schema (
Version INTEGER
);
`
// Establish connection to sqlite database
func ConnectDatabase() {
var err error
// Try using sqlite as our database
sqlPath := utils.GetFilePath(sqlFile)
db, err = sqlx.Open("sqlite", sqlPath)
if err != nil {
log.Printf("Error opening sqlite database connection to file '%s' : '%s'\n", sqlPath, err)
os.Exit(1)
} else {
log.Printf("Connected to sqlite database file '%s'\n", sqlPath)
}
//sqlx.NameMapper = func(s string) string { return s }
// Make sure our tables exist
CreateTables()
//defer db.Close()
}
func DisconnectDatabase() {
log.Printf("DisconnectDatabase called")
defer db.Close()
}
func CreateTables() {
var err error
var rowCount int
// Create database tables if it doesn't exist
// Roles table should go first since other tables refer to it
if _, err = db.Exec(createRoles); err != nil {
log.Printf("Error checking roles table : '%s'", err)
os.Exit(1)
}
rowCount, _ = CheckCount("roles")
if rowCount == 0 {
if _, err = db.Exec("INSERT INTO roles VALUES(1, 'Admin', false, true);"); err != nil {
log.Printf("Error adding initial admin role : '%s'", err)
os.Exit(1)
}
if _, err = db.Exec("INSERT INTO roles VALUES(2, 'UserRole', false, false);"); err != nil {
log.Printf("Error adding initial user role : '%s'", err)
os.Exit(1)
}
if _, err = db.Exec("INSERT INTO roles VALUES(3, 'GuestRole', true, false);"); err != nil {
log.Printf("Error adding initial guest role : '%s'", err)
os.Exit(1)
}
}
// Users table
if _, err = db.Exec(createUsers); err != nil {
log.Printf("Error checking users table : '%s'", err)
os.Exit(1)
}
rowCount, _ = CheckCount("users")
if rowCount == 0 {
// Check if there was an initial password defined in the .env file
initialPassword := os.Getenv("INITIAL_PASSWORD")
if initialPassword == "" {
initialPassword = "password"
} else if initialPassword[:4] == "$2a$" {
log.Printf("CreateTables inital admin password is already a hash")
} else {
cryptText, _ := bcrypt.GenerateFromPassword([]byte(initialPassword), bcrypt.DefaultCost)
initialPassword = string(cryptText)
}
if _, err = db.Exec("INSERT INTO users VALUES(1, 1, 'Administrator', ?);", initialPassword); err != nil {
log.Printf("Error adding initial admin role : '%s'", err)
os.Exit(1)
}
}
// Secrets table
if _, err = db.Exec(createSecrets); err != nil {
log.Printf("Error checking secrets table : '%s'", err)
os.Exit(1)
}
// Schema table should go last so we know if the database has a value in the schema table then everything was created properly
if _, err = db.Exec(createSchema); err != nil {
log.Printf("Error checking schema table : '%s'", err)
os.Exit(1)
}
schemaCheck, _ := CheckColumnExists("schema", "Version")
if !schemaCheck {
if _, err = db.Exec("INSERT INTO schema VALUES(1);"); err != nil {
log.Printf("Error adding initial scehama version : '%s'", err)
os.Exit(1)
}
}
}
// Count the number of records in the sqlite database
// Borrowed from https://gist.github.com/trkrameshkumar/f4f1c00ef5d578561c96?permalink_comment_id=2687592#gistcomment-2687592
func CheckCount(tablename string) (int, error) {
var count int
stmt, err := db.Prepare("SELECT COUNT(*) as count FROM " + tablename)
if err != nil {
log.Printf("CheckCount error preparing sqlite statement : '%s'\n", err)
return 0, err
}
err = stmt.QueryRow().Scan(&count)
if err != nil {
log.Printf("CheckCount error querying database record count : '%s'\n", err)
return 0, err
}
stmt.Close() // or use defer rows.Close(), idc
return count, nil
}
// From https://stackoverflow.com/a/60100045
func GenerateInsertMethod(q interface{}) (string, error) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
query := fmt.Sprintf("INSERT INTO %s", reflect.TypeOf(q).Name())
fieldNames := ""
fieldValues := ""
v := reflect.ValueOf(q)
for i := 0; i < v.NumField(); i++ {
if i == 0 {
fieldNames = fmt.Sprintf("%s%s", fieldNames, v.Type().Field(i).Name)
} else {
fieldNames = fmt.Sprintf("%s, %s", fieldNames, v.Type().Field(i).Name)
}
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
fieldValues = fmt.Sprintf("%s%d", fieldValues, v.Field(i).Int())
} else {
fieldValues = fmt.Sprintf("%s, %d", fieldValues, v.Field(i).Int())
}
case reflect.String:
if i == 0 {
fieldValues = fmt.Sprintf("%s\"%s\"", fieldValues, v.Field(i).String())
} else {
fieldValues = fmt.Sprintf("%s, \"%s\"", fieldValues, v.Field(i).String())
}
case reflect.Bool:
var boolSet int8
if v.Field(i).Bool() {
boolSet = 1
}
if i == 0 {
fieldValues = fmt.Sprintf("%s%d", fieldValues, boolSet)
} else {
fieldValues = fmt.Sprintf("%s, %d", fieldValues, boolSet)
}
default:
log.Printf("Unsupported type '%s'\n", v.Field(i).Kind())
}
}
query = fmt.Sprintf("%s(%s) VALUES (%s)", query, fieldNames, fieldValues)
return query, nil
}
return "", errors.New("SqlGenerationError")
}
func CheckColumnExists(table string, column string) (bool, error) {
var count int64
rows, err := db.Queryx("SELECT COUNT(*) AS CNTREC FROM pragma_table_info('" + table + "') WHERE name='" + column + "';")
if err != nil {
log.Printf("CheckColumnExists error querying database for existence of column '%s' : '%s'\n", column, err)
return false, err
}
defer rows.Close()
for rows.Next() {
// cols is an []interface{} of all of the column results
cols, _ := rows.SliceScan()
log.Printf("CheckColumnExists Value is '%v' for table '%s' and column '%s'\n", cols[0].(int64), table, column)
count = cols[0].(int64)
if count == 1 {
return true, nil
} else {
return false, nil
}
}
err = rows.Err()
if err != nil {
log.Printf("CheckColumnExists error getting results : '%s'\n", err)
return false, err
}
return false, nil
}

View File

@@ -1,20 +1,26 @@
package models
import (
"ccsecrets/utils/token"
"database/sql"
"errors"
"fmt"
"log"
"net/http"
"smt/utils/token"
"strings"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
type User struct {
UserId int `db:"UserId"`
RoleId int `db:"RoleId"`
UserName string `db:"UserName"`
UserId int `db:"UserId" json:"userId"`
GroupId int `db:"GroupId" json:"groupId"`
UserName string `db:"UserName" json:"userName"`
Password string `db:"Password" json:"-"`
LdapUser bool `db:"LdapUser" json:"ldapUser"`
Admin bool `db:"Admin"`
LastLogin time.Time `db:"LastLogin" json:"lastLogin"`
LdapGroup bool `db:"LdapGroup"`
}
type UserRole struct {
@@ -24,13 +30,37 @@ type UserRole struct {
Admin bool `db:"Admin"`
}
type UserGroup struct {
User
GroupName string `db:"GroupName"`
LdapGroup bool `db:"LdapGroup"`
LdapDn string `db:"LdapDn"`
Admin bool `db:"Admin"`
}
// Combine Users and Safes to determine which safes a user has access to
type UserSafe struct {
User
SafeId int `db:"SafeId"`
SafeName string `db:"SafeName"`
ReadOnly bool `db:"ReadOnly" json:"readOnly"`
//GroupId int `db:"GroupId"`
}
func (u *User) SaveUser() (*User, error) {
var err error
// TODO - validate username not already in use
if u.LastLogin.IsZero() {
u.LastLogin = time.Time{}
}
result, err := db.NamedExec((`INSERT INTO users (RoleId, UserName, Password) VALUES (:RoleId, :UserName, :Password)`), u)
// Validate username not already in use
_, err = UserGetByName(u.UserName)
if err != nil && err.Error() == "user not found" {
log.Printf("SaveUser confirmed no existing user, continuing with creation of user '%s'\n", u.UserName)
//log.Printf("u: %v\n", u)
result, err := db.NamedExec((`INSERT INTO users (GroupId, UserName, Password, LdapUser, Admin, LastLogin) VALUES (:GroupId, :UserName, :Password, :LdapUser, :Admin, :LastLogin)`), u)
if err != nil {
log.Printf("SaveUser error executing sql record : '%s'\n", err)
@@ -38,32 +68,126 @@ func (u *User) SaveUser() (*User, error) {
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
u.UserId = int(id)
log.Printf("SaveUser insert returned result id '%d' affecting %d row(s).\n", id, affected)
}
} else {
log.Printf("SaveUser Username already exists : '%v'\n", err)
}
return u, nil
}
func (u *User) DeleteUser() error {
// Validate username exists
_, err := UserGetByName(u.UserName)
if err != nil {
log.Printf("DeleteUser error finding user account to remove : '%s'\n", err)
return err
} else {
log.Printf("DeleteUser confirmed user exists, continuing with deletion of user '%s'\n", u.UserName)
result, err := db.NamedExec((`DELETE FROM users WHERE UserName = :UserName`), u)
if err != nil {
log.Printf("DeleteUser error executing sql delete : '%s'\n", err)
return err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("DeleteUser returned result id '%d' affecting %d row(s).\n", id, affected)
}
}
return nil
}
func VerifyPassword(password, hashedPassword string) error {
log.Printf("VerifyPassword comparing input against hashed value '%s'\n", hashedPassword)
if len(password) == 0 {
return errors.New("unable to verify empty password")
}
if len(hashedPassword) == 0 {
return errors.New("unable to compare password with empty hash")
}
//log.Printf("VerifyPassword comparing input against hashed value '%s'\n", hashedPassword)
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
func LoginCheck(username string, password string) (string, error) {
var err error
newLdapUser := false
u := User{}
// Query database for matching user object
err = db.QueryRowx("SELECT * FROM Users WHERE Username=?", username).StructScan(&u)
// Use IFNULL to handle situation where a user might not be a member of a group
// Join on groups table so we can get the value in LdapGroup column
log.Printf("LoginCheck retrieved user '%v' from database\n", u)
if err != nil {
return "", err
// if username is UPN format then get just the user portion
if strings.Contains(username, "@") {
plainUser := GetUserFromUPN(username)
// check for original username or plainUser
err = db.QueryRowx(`
SELECT users.UserId, IFNULL(users.GroupId, 0) GroupId, UserName, Password, LdapUser, users.Admin, groups.LdapGroup FROM Users
INNER JOIN groups ON users.GroupId = groups.GroupId
WHERE Username=? OR Username=?`, username, plainUser).StructScan(&u)
} else {
err = db.QueryRowx(`
SELECT users.UserId, IFNULL(users.GroupId, 0) GroupId, UserName, Password, LdapUser, users.Admin, groups.LdapGroup FROM Users
INNER JOIN groups ON users.GroupId = groups.GroupId
WHERE Username=?`, username).StructScan(&u)
}
if err != nil {
if err == sql.ErrNoRows {
log.Printf("LoginCheck found no users matching username '%s'\n", username)
// TODO - if username contains UPN style login then try extracting just the username and doing a query on that
// check LDAP if enabled
if LdapEnabled {
log.Printf("LoginCheck initiating ldap lookup for username '%s'\n", username)
ldapUser, err := UserLdapNewLoginCheck(username, password)
if err != nil {
errString := fmt.Sprintf("LoginCheck error checking LDAP for user : '%s'\n", err)
log.Print(errString)
return "", errors.New(errString)
}
if ldapUser == (User{}) {
errString := fmt.Sprintf("LoginCheck user not found in LDAP : '%s'\n", err)
log.Print(errString)
return "", errors.New(errString)
} else {
log.Printf("LoginCheck verified LDAP user successfully\n")
u = ldapUser
// Since this user wasn't in the database, they must have been logging in for the first time
// So we don't need to repeat the ldap bind and verification
newLdapUser = true
}
} else {
// LDAP is not enabled, if user is not in the database then they can't login
return "", errors.New("specified user not found in database")
}
} else {
errString := fmt.Sprintf("LoginCheck error querying database : '%s'\n", err)
log.Print(errString)
return "", errors.New(errString)
}
} else {
//log.Printf("LoginCheck retrieved user '%v' from database\n", u)
log.Printf("LoginCheck retrieved user id '%d' from database\n", u.UserId)
}
//log.Printf("u: %v\n", u)
if !u.LdapUser {
// Locally defined user, perform password verification
err = VerifyPassword(password, u.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
@@ -72,7 +196,44 @@ func LoginCheck(username string, password string) (string, error) {
} else {
log.Printf("LoginCheck verified password against stored hash.\n")
}
} else {
// LDAP user, verify credential if user wasn't logging in for the first time
if !newLdapUser {
err := VerifyLdapCreds(username, password)
if err != nil {
errString := fmt.Sprintf("LoginCheck LDAP user bind unsuccessful : '%s'\n", err)
log.Print(errString)
return "", errors.New(errString)
} else {
log.Printf("LoginCheck successfully verified LDAP user\n")
// check if user's group membership is an ldap group or not
log.Printf("User id '%d' is a member of group '%d' which has ldapGroup status '%v'\n", u.UserId, u.GroupId, u.LdapGroup)
// If user's group membership is an ldap group, then run UserLdapGroupVerify as we were doing before
if u.LdapGroup {
// confirm that current LDAP group membership matches a group
err := UserLdapGroupVerify(username, password)
if err != nil {
// No valid group membership
errString := fmt.Sprintf("ldap group membership check unsuccessful : '%s'\n", err)
log.Printf("LoginCheck %s\n", errString)
return "", errors.New(errString)
}
} else { // If user's group membership is not an ldap group, then we are fine and the login attempt was successful
log.Printf("No need to check ldap group membership since user is not a member of an ldap group\n")
}
}
} else {
log.Printf("LoginCheck no need to repeat LDAP bind for new user login\n")
}
}
// If we reached this point then the login was successful
// Generate a new token and return it to the user
log.Printf("LoginCheck generating token for user id '%d'\n", uint(u.UserId))
token, err := token.GenerateToken(uint(u.UserId))
if err != nil {
@@ -80,11 +241,106 @@ func LoginCheck(username string, password string) (string, error) {
return "", err
}
u.UserSetLastLogin()
return token, nil
}
func GetUserByID(uid uint) (User, error) {
// UserLdapGroupVerify will check current group membership and generate an error if match is not found in database
func UserLdapGroupVerify(username string, password string) error {
// try to get LDAP group membership
ldapGroups, err := LdapGetGroupMembership(username, password)
if err != nil {
return err
}
// Compare all roles against the list of user's group membership
groupList, err := GroupList()
if err != nil {
return err
}
for _, group := range groupList {
for _, lg := range ldapGroups {
if group.LdapDn == lg {
log.Printf("Found match with groupname '%s' and LDAP group DN '%s', user is a member of group ID '%d'\n", group.GroupName, group.LdapDn, group.GroupId)
return nil
}
}
}
return errors.New("no match between database and ldap group membership")
}
// UserLdapNewLoginCheck will verify group membership and save User into database
func UserLdapNewLoginCheck(username string, password string) (User, error) {
var u User
u.UserName = username
// try to get LDAP group membership
ldapGroups, err := LdapGetGroupMembership(username, password)
if err != nil {
if err.Error() == "invalid user credentials" {
return u, nil
} else {
return u, err
}
}
// Compare all roles against the list of user's group membership
groupList, err := GroupList()
if err != nil {
return u, err
}
matchFound := false
for _, group := range groupList {
// Skip any groups that aren't LDAP groups
if len(group.LdapDn) == 0 {
continue
}
for _, lg := range ldapGroups {
if group.LdapDn == lg {
log.Printf("Found match with groupname '%s' and LDAP group DN '%s', user is a member of group ID '%d'\n", group.GroupName, group.LdapDn, group.GroupId)
u.GroupId = group.GroupId
matchFound = true
break
} else {
log.Printf("Groupname '%s' with LDAP group '%s' not match user group '%s'\n", group.GroupName, group.LdapDn, lg)
}
}
}
if matchFound {
// If we found a match, then store user with appropriate role ID
u.LdapUser = true
u.SaveUser()
}
return u, nil
}
func (u *User) UserSetLastLogin() error {
u.LastLogin = time.Now().UTC()
result, err := db.NamedExec((`UPDATE users SET LastLogin = :LastLogin WHERE UserId = :UserId`), u)
if err != nil {
log.Printf("UserSetLastLogin error executing sql update : '%s'\n", err)
return err
} else {
affected, _ := result.RowsAffected()
id, _ := result.LastInsertId()
log.Printf("UserSetLastLogin returned result id '%d' affecting %d row(s).\n", id, affected)
}
return nil
}
func UserGetByID(uid uint) (User, error) {
var u User
@@ -104,7 +360,7 @@ func GetUserByID(uid uint) (User, error) {
}
func GetUserByName(username string) (User, error) {
func UserGetByName(username string) (User, error) {
var u User
@@ -117,13 +373,14 @@ func GetUserByName(username string) (User, error) {
return u, nil
}
/*
func GetUserRoleByID(uid uint) (UserRole, error) {
var ur UserRole
// Query database for matching user object
log.Printf("GetUserRoleByID querying for userid '%d'\n", uid)
err := db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", uid).StructScan(&ur)
err := db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, users.LdapUser, users.LdapDn, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", uid).StructScan(&ur)
if err != nil {
log.Printf("GetUserRoleByID received error when querying database : '%s'\n", err)
return ur, errors.New("GetUserRoleByID user not found")
@@ -131,8 +388,26 @@ func GetUserRoleByID(uid uint) (UserRole, error) {
return ur, nil
}
*/
func GetUserRoleFromToken(c *gin.Context) (UserRole, error) {
func UserGetGroupByID(uid uint) (UserGroup, error) {
var ug UserGroup
// Query database for matching user object
log.Printf("UserGetGroupByID querying for userid '%d'\n", uid)
err := db.QueryRowx("SELECT users.UserId, users.GroupId, users.UserName, users.Password, users.LdapUser, groups.GroupName, groups.LdapGroup, groups.LdapDn, groups.Admin FROM users INNER JOIN groups ON users.GroupId = groups.GroupId WHERE users.UserId=?", uid).StructScan(&ug)
if err != nil {
log.Printf("UserGetGroupByID received error when querying database : '%s'\n", err)
return ug, errors.New("UserGetGroupByID user id not found")
}
return ug, nil
}
/*
func UserGetRoleFromToken(c *gin.Context) (UserRole, error) {
var ur UserRole
@@ -144,18 +419,123 @@ func GetUserRoleFromToken(c *gin.Context) (UserRole, error) {
}
// Query database for matching user object
log.Printf("GetUserRoleFromToken querying for userid '%d'\n", user_id)
err = db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", user_id).StructScan(&ur)
log.Printf("UserGetRoleFromToken querying for userid '%d'\n", user_id)
err = db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, users.LdapUser, users.LdapDn, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", user_id).StructScan(&ur)
if err != nil {
log.Printf("GetUserRoleFromToken received error when querying database : '%s'\n", err)
return ur, errors.New("GetUserRoleFromToken user not found")
log.Printf("UserGetRoleFromToken received error when querying database : '%s'\n", err)
return ur, errors.New("UserGetRoleFromToken user not found")
}
return ur, nil
}
*/
func UserGetSafesAllowed(userId int) ([]UserSafe, error) {
var results []UserSafe
// TODO add union for permissions directly assigned to safe via UserId instead of GroupId
// join users, groups and permissions
rows, err := db.Queryx(`
SELECT users.UserId, users.GroupId, users.UserName,
permissions.SafeId, permissions.ReadOnly, safes.SafeName FROM users
INNER JOIN groups ON users.GroupId = groups.GroupId
INNER JOIN permissions ON groups.GroupId = permissions.GroupId
INNER JOIN safes on permissions.SafeId = safes.SafeId
WHERE users.UserId=?`, userId)
if err != nil {
log.Printf("UserGetSafesAllowed error executing sql record : '%s'\n", err)
return results, err
} else {
defer rows.Close()
// Get columns from rows for debugging
/*
columns, err := rows.Columns()
if err != nil {
log.Printf("UserGetSafesAllowed error getting column listing : '%s'\n", err)
return results, err
}
log.Printf("columns: %v\n", columns)
*/
// parse all the results into a slice
for rows.Next() {
var us UserSafe
err = rows.StructScan(&us)
if err != nil {
log.Printf("UserGetSafesAllowed error parsing sql record : '%s'\n", err)
return results, err
}
//log.Printf("UserGetSafesAllowed adding record : '%+v'\n", us)
//debugPrint := utils.PrintStructContents(&us, 0)
//log.Printf("UserGetSafesAllowed adding record :\n%s\n", debugPrint)
results = append(results, us)
/*
func (u *User) PrepareGive() {
u.Password = ""
// For intense debugging
// Create a map to store column names and values
rowValues := make(map[string]interface{})
// Scan each row into the map
err := rows.MapScan(rowValues)
if err != nil {
log.Println(err)
continue
}
// Print the raw row record
log.Println("-----------")
for _, column := range columns {
log.Printf("%s: %v\n", column, rowValues[column])
}
log.Println("-----------")
*/
}
//log.Printf("UserGetSafesAllowed retrieved '%d' results\n", len(results))
}
return results, nil
}
func UserList() ([]User, error) {
var results []User
// Query database for role definitions
rows, err := db.Queryx("SELECT * FROM users")
if err != nil {
log.Printf("UserList error executing sql record : '%s'\n", err)
return results, err
} else {
// parse all the results into a slice
for rows.Next() {
var u User
err = rows.StructScan(&u)
if err != nil {
log.Printf("UserList error parsing sql record : '%s'\n", err)
return results, err
}
results = append(results, u)
}
log.Printf("UserList retrieved '%d' results\n", len(results))
}
return results, nil
}
func UserCheckIfAdmin(userId int) bool {
// TODO
u, err := UserGetByID(uint(userId))
if err != nil {
log.Printf("UserCheckIfAdmin received error : '%s'\n", err)
return false
}
return u.Admin
}

84
template.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
$for(author-meta)$
<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
<meta name="dcterms.date" content="$date-meta$" />
$endif$
$if(keywords)$
<meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
$endif$
<title>$if(title-prefix)$$title-prefix$ $endif$$pagetitle$</title>
$if(highlighting-css)$
<style type="text/css">
$highlighting-css$
</style>
$endif$
$for(css)$
<link rel="stylesheet" href="$css$" />
$endfor$
<style type="text/css">
nav {
justify-content: space-around;
}
$if(quotes)$
q { quotes: "“" "”" "" ""; }
$endif$
</style>
$if(math)$
$math$
$endif$
$for(header-includes)$
$header-includes$
$endfor$
</head>
<body>
$for(include-before)$
$include-before$
$endfor$
$if(title)$
<header id="title-block-header">
<nav id="$idprefix$TOC">
<!--<a href="/"><img alt="Logo" src="$logo$" height="70"></a>-->
$if(toc)$
<ul>
<li><a href="#">Menu</a>
$table-of-contents$
</li>
</ul>
$endif$
</nav>
<h1 class="title">$title$</h1>
$if(subtitle)$
<p class="subtitle">$subtitle$</p>
$endif$
$for(author)$
<p class="author">$author$</p>
$endfor$
$if(date)$
<p class="date">$date$</p>
$endif$
$if(website)$
<p><a href="$website$"><i>Website &nearr;</i></a></p>
$endif$
</header>
$endif$
<main>
<hr>
<article>
$body$
$for(include-after)$
$include-after$
$endfor$
<hr>
</article>
</main>
</body>
</html>

12
test.env Normal file
View File

@@ -0,0 +1,12 @@
LOG_FILE=/var/log/smt.log
API_SECRET=ArCRbRoIyN2tTaG613v4skKHHZtXZ0Vy
INITIAL_PASSWORD=Password123
TOKEN_HOUR_LIFESPAN=168
BIND_IP=
BIND_PORT=8443
LDAP_BIND_ADDRESS=adcp12.cdc.home
LDAP_BASE_DN=CN=Users,DC=cdc,DC=home
LDAP_TRUST_CERT_FILE=
LDAP_INSECURE_VALIDATION=true
TLS_KEY_FILE=key.pem
TLS_CERT_FILE=cert.pem

139
utils/certOperations.go Normal file
View File

@@ -0,0 +1,139 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"os"
"path/filepath"
"time"
)
func GenerateCerts(tlsCert string, tlsKey string) {
// @see https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
// @see https://golang.org/src/crypto/tls/generate_cert.go
validFrom := ""
validFor := 365 * 24 * time.Hour
isCA := true
// Get the hostname
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
// Check that the directory exists
relativePath := filepath.Dir(tlsCert)
log.Printf("GenerateCerts relative path for file creation is '%s'\n", relativePath)
_, err = os.Stat(relativePath)
if os.IsNotExist(err) {
log.Printf("Certificate path does not exist, creating %s before generating certificate\n", relativePath)
os.MkdirAll(relativePath, os.ModePerm)
}
// Generate a private key
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
log.Fatalf("Failed to generate private key: %v", err)
}
var notBefore time.Time
if len(validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
}
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("Failed to generate serial number: %v", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"DTMS"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
template.DNSNames = append(template.DNSNames, hostname)
// Add in all the non-local IPs
ifaces, err := net.Interfaces()
if err != nil {
log.Printf("Error enumerating interfaces: %v\n", err)
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
log.Printf("Oops: %v\n", err)
}
for _, address := range addrs {
// check the address type and if it is not a loopback then add it to the list
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
template.IPAddresses = append(template.IPAddresses, ipnet.IP)
}
}
}
}
if isCA {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
}
certOut, err := os.Create(tlsCert)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", tlsCert, err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", tlsCert, err)
}
if err := certOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", tlsCert, err)
}
log.Printf("wrote %s\n", tlsCert)
keyOut, err := os.OpenFile(tlsKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", tlsKey, err)
return
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", tlsKey, err)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", tlsKey, err)
}
log.Printf("wrote %s\n", tlsKey)
}

121
utils/structOperations.go Normal file
View File

@@ -0,0 +1,121 @@
package utils
import (
"fmt"
"reflect"
"strings"
"time"
)
/*
func PrintStructContents(s interface{}, indentLevel int) string {
var result strings.Builder
val := reflect.ValueOf(s)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
log.Printf("PrintStructContents [%d] field '%s' (%T)\n", i, field, fieldType)
indent := strings.Repeat("\t", indentLevel)
result.WriteString(fmt.Sprintf("%s%s: ", indent, fieldType.Name))
log.Printf("%s%s: \n", indent, fieldType.Name)
switch field.Kind() {
case reflect.Struct:
result.WriteString("\n")
foo := PrintStructContents(field.Interface(), indentLevel+1)
log.Printf("foo: %s\n", foo)
result.WriteString(foo)
case reflect.Uint64:
log.Printf("uint64 %v\n", field.Interface())
result.WriteString(fmt.Sprintf("%v\n", field.Interface()))
default:
log.Printf("default %v\n", field.Interface())
result.WriteString(fmt.Sprintf("%v\n", field.Interface()))
}
}
log.Printf("PrintStructContents completed\n")
return result.String()
}
*/
func PrintStructContents(s interface{}, indentLevel int) string {
var result strings.Builder
val := reflect.ValueOf(s)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
indent := strings.Repeat("\t", indentLevel)
result.WriteString(fmt.Sprintf("%s%s: ", indent, fieldType.Name))
switch field.Kind() {
case reflect.Struct:
if fieldType.Type == reflect.TypeOf(time.Time{}) {
// Handle time.Time field
result.WriteString(fmt.Sprintf("%v\n", field.Interface().(time.Time).Format("2006-12-24 15:04:05")))
} else {
result.WriteString("\n")
result.WriteString(PrintStructContents(field.Interface(), indentLevel+1))
}
default:
result.WriteString(fmt.Sprintf("%v\n", field.Interface()))
}
}
return result.String()
}
type Identifiable interface {
GetId() int
}
// AppendIfNotExists requires a struct to implement the GetId() method
// Then we can use this function to avoid creating duplicate entries in the slice
func AppendIfNotExists[T Identifiable](slice []T, element T) []T {
for _, existingElement := range slice {
if existingElement.GetId() == element.GetId() {
// Element with the same Id already exists, don't append
return slice
}
}
// Element with the same Id does not exist, append the new element
return append(slice, element)
}
// UpdateStruct updates the values in the destination struct with values from the source struct
func UpdateStruct(dest interface{}, src interface{}) {
destValue := reflect.ValueOf(dest).Elem()
srcValue := reflect.ValueOf(src).Elem()
destType := destValue.Type()
for i := 0; i < srcValue.NumField(); i++ {
srcField := srcValue.Field(i)
destField := destValue.FieldByName(destType.Field(i).Name)
if destField.IsValid() && destField.Type() == srcField.Type() && destField.CanSet() {
destField.Set(srcField)
}
}
}

View File

@@ -8,8 +8,9 @@ import (
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
//jwt "github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
jwt "github.com/golang-jwt/jwt/v5"
)
func GenerateToken(user_id uint) (string, error) {
@@ -24,6 +25,7 @@ func GenerateToken(user_id uint) (string, error) {
claims["authorized"] = true
claims["user_id"] = user_id
claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
// https://pkg.go.dev/github.com/golang-jwt/jwt/v5#New
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("API_SECRET")))
@@ -36,8 +38,8 @@ func TokenValid(c *gin.Context) error {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
//return []byte(os.Getenv("API_SECRET")), nil
return []byte(""), nil
// This code says signature is invalid if we return an empty []byte but I don't know why
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
return err
@@ -51,6 +53,8 @@ func ExtractToken(c *gin.Context) string {
return token
}
bearerToken := c.Request.Header.Get("Authorization")
//log.Printf("ExtractToken extracted token '%v'\n", bearerToken)
if len(strings.Split(bearerToken, " ")) == 2 {
return strings.Split(bearerToken, " ")[1]
}
@@ -64,20 +68,23 @@ func ExtractTokenID(c *gin.Context) (uint, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Why return the secret??
//return []byte(os.Getenv("API_SECRET")), nil
return 0, nil
// Why return the secret?? Code doesn't work if we don't return the secret
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
log.Printf("ExtractTokenID error parsing token : '%s'\n", err)
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)
if err != nil {
log.Printf("ExtractTokenID checking token valid : '%s'\n", err)
return 0, err
}
return uint(uid), nil
}
log.Printf("ExtractTokenID unknown error\n")
return 0, nil
}

View File

@@ -1,17 +1,10 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"os"
"path/filepath"
"time"
)
const rsaBits = 4096
@@ -59,127 +52,3 @@ func FileExists(filename string) bool {
}
return !info.IsDir()
}
func GenerateCerts(tlsCert string, tlsKey string) {
// @see https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
// @see https://golang.org/src/crypto/tls/generate_cert.go
validFrom := ""
validFor := 365 * 24 * time.Hour
isCA := true
// Get the hostname
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
// Check that the directory exists
relativePath := filepath.Dir(tlsCert)
log.Printf("GenerateCerts relative path for file creation is '%s'\n", relativePath)
_, err = os.Stat(relativePath)
if os.IsNotExist(err) {
log.Printf("Certificate path does not exist, creating %s before generating certificate\n", relativePath)
os.MkdirAll(relativePath, os.ModePerm)
}
// Generate a private key
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
log.Fatalf("Failed to generate private key: %v", err)
}
var notBefore time.Time
if len(validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
}
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("Failed to generate serial number: %v", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"DTMS"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
template.DNSNames = append(template.DNSNames, hostname)
// Add in all the non-local IPs
ifaces, err := net.Interfaces()
if err != nil {
log.Printf("Error enumerating interfaces: %v\n", err)
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
log.Printf("Oops: %v\n", err)
}
for _, address := range addrs {
// check the address type and if it is not a loopback then add it to the list
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
template.IPAddresses = append(template.IPAddresses, ipnet.IP)
}
}
}
}
if isCA {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
}
certOut, err := os.Create(tlsCert)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", tlsCert, err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", tlsCert, err)
}
if err := certOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", tlsCert, err)
}
log.Printf("wrote %s\n", tlsCert)
keyOut, err := os.OpenFile(tlsKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", tlsKey, err)
return
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", tlsKey, err)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", tlsKey, err)
}
log.Printf("wrote %s\n", tlsKey)
}

BIN
www/database.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
www/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

BIN
www/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
www/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

536
www/mvp.css Normal file
View File

@@ -0,0 +1,536 @@
/* MVP.css v1.14 - https://github.com/andybrewer/mvp */
:root {
--active-brightness: 0.85;
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color-accent: #118bee15;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-link: #118bee;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-table: #118bee;
--color-text: #000;
--color-text-secondary: #999;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-table: left;
--justify-normal: left;
--line-height: 1.5;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
/* --width-content: 1080px;*/
}
@media (prefers-color-scheme: dark) {
:root[color-mode="user"] {
--color-accent: #0097fc4f;
--color-bg: #333;
--color-bg-secondary: #555;
--color-link: #0097fc;
--color-secondary: #e20de9;
--color-secondary-accent: #e20de94f;
--color-shadow: #bbbbbb20;
--color-table: #0097fc;
--color-text: #f7f7f7;
--color-text-secondary: #aaa;
}
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Layout */
article aside {
background: var(--color-secondary-accent);
border-left: 4px solid var(--color-secondary);
padding: 0.01rem 0.8rem;
}
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
line-height: var(--line-height);
margin: 0;
overflow-x: hidden;
padding: 0;
}
footer,
header,
main {
margin: 0 auto;
max-width: var(--width-content);
padding: 1rem 1rem;
}
hr {
background-color: var(--color-bg-secondary);
border: none;
height: 1px;
margin: 2rem 0;
width: 100%;
}
section {
display: flex;
flex-wrap: wrap;
justify-content: var(--justify-important);
}
section img,
article img {
max-width: 100%;
}
section pre {
overflow: auto;
}
section aside {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
margin: 1rem;
padding: 1.25rem;
width: var(--width-card);
}
section aside:hover {
box-shadow: var(--box-shadow) var(--color-bg-secondary);
}
[hidden] {
display: none;
}
/* Headers */
article header,
div header,
main header {
padding-top: 0;
}
header {
text-align: var(--justify-important);
}
header a b,
header a em,
header a i,
header a strong {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
header nav img {
margin: 1rem 0;
}
section header {
padding-top: 0;
width: 100%;
}
/* Nav */
nav {
align-items: center;
display: flex;
font-weight: bold;
justify-content: space-between;
margin-bottom: 7rem;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline-block;
margin: 0 0.5rem;
position: relative;
text-align: left;
}
/* Nav Dropdown */
nav ul li:hover ul {
display: block;
}
nav ul li ul {
background: var(--color-bg);
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: none;
height: auto;
left: -2px;
padding: .5rem 1rem;
position: absolute;
top: 1.7rem;
white-space: nowrap;
width: auto;
z-index: 1;
}
nav ul li ul::before {
/* fill gap above to make mousing over them easier */
content: "";
position: absolute;
left: 0;
right: 0;
top: -0.5rem;
height: 0.5rem;
}
nav ul li ul li,
nav ul li ul li a {
display: block;
}
/* Typography */
code,
samp {
background-color: var(--color-accent);
border-radius: var(--border-radius);
color: var(--color-text);
display: inline-block;
margin: 0 0.1rem;
padding: 0 0.5rem;
}
details {
margin: 1.3rem 0;
}
details summary {
font-weight: bold;
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: var(--line-height);
}
mark {
padding: 0.1rem;
}
ol li,
ul li {
padding: 0.2rem 0;
}
p {
margin: 0.75rem 0;
padding: 0;
width: 100%;
}
pre {
margin: 1rem 0;
max-width: var(--width-card-wide);
padding: 1rem 0;
}
pre code,
pre samp {
display: block;
max-width: var(--width-card-wide);
padding: 0.5rem 2rem;
white-space: pre-wrap;
}
small {
color: var(--color-text-secondary);
}
sup {
background-color: var(--color-secondary);
border-radius: var(--border-radius);
color: var(--color-bg);
font-size: xx-small;
font-weight: bold;
margin: 0.2rem;
padding: 0.2rem 0.3rem;
position: relative;
top: -2px;
}
/* Links */
a {
color: var(--color-link);
display: inline-block;
font-weight: bold;
text-decoration: underline;
}
a:active {
filter: brightness(var(--active-brightness));
}
a:hover {
filter: brightness(var(--hover-brightness));
}
a b,
a em,
a i,
a strong,
button,
input[type="submit"] {
border-radius: var(--border-radius);
display: inline-block;
font-size: medium;
font-weight: bold;
line-height: var(--line-height);
margin: 0.5rem 0;
padding: 1rem 2rem;
}
button,
input[type="submit"] {
font-family: var(--font-family);
}
button:active,
input[type="submit"]:active {
filter: brightness(var(--active-brightness));
}
button:hover,
input[type="submit"]:hover {
cursor: pointer;
filter: brightness(var(--hover-brightness));
}
a b,
a strong,
button,
input[type="submit"] {
background-color: var(--color-link);
border: 2px solid var(--color-link);
color: var(--color-bg);
}
a em,
a i {
border: 2px solid var(--color-link);
border-radius: var(--border-radius);
color: var(--color-link);
display: inline-block;
padding: 1rem 2rem;
}
article aside a {
color: var(--color-secondary);
}
/* Images */
figure {
margin: 0;
padding: 0;
}
figure img {
max-width: 100%;
}
figure figcaption {
color: var(--color-text-secondary);
}
/* Forms */
button:disabled,
input:disabled {
background: var(--color-bg-secondary);
border-color: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: not-allowed;
}
button[disabled]:hover,
input[type="submit"][disabled]:hover {
filter: none;
}
form {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: block;
max-width: var(--width-card-wide);
min-width: var(--width-card);
padding: 1.5rem;
text-align: var(--justify-normal);
}
form header {
margin: 1.5rem 0;
padding: 1.5rem 0;
}
input,
label,
select,
textarea {
display: block;
font-size: inherit;
max-width: var(--width-card-wide);
}
input[type="checkbox"],
input[type="radio"] {
display: inline-block;
}
input[type="checkbox"]+label,
input[type="radio"]+label {
display: inline-block;
font-weight: normal;
position: relative;
top: 1px;
}
input[type="range"] {
padding: 0.4rem 0;
}
input,
select,
textarea {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
padding: 0.4rem 0.8rem;
}
input[type="text"],
textarea {
width: calc(100% - 1.6rem);
}
input[readonly],
textarea[readonly] {
background-color: var(--color-bg-secondary);
}
label {
font-weight: bold;
margin-bottom: 0.2rem;
}
/* Popups */
dialog {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
z-index: 999;
}
/* Tables */
table {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
border-spacing: 0;
display: inline-block;
max-width: 100%;
overflow-x: auto;
padding: 0;
/*white-space: nowrap;*/
}
table td,
table th,
table tr {
padding: 0.4rem 0.8rem;
text-align: var(--justify-table);
}
table thead {
background-color: var(--color-table);
border-collapse: collapse;
border-radius: var(--border-radius);
color: var(--color-bg);
margin: 0;
padding: 0;
}
table thead th:first-child {
border-top-left-radius: var(--border-radius);
}
table thead th:last-child {
border-top-right-radius: var(--border-radius);
}
table thead th:first-child,
table tr td:first-child {
text-align: var(--justify-normal);
}
table tr:nth-child(even) {
background-color: var(--color-accent);
}
/* Quotes */
blockquote {
display: block;
font-size: x-large;
line-height: var(--line-height);
margin: 1rem auto;
max-width: var(--width-card-medium);
padding: 1.5rem 1rem;
text-align: var(--justify-important);
}
blockquote footer {
color: var(--color-text-secondary);
display: block;
font-size: small;
line-height: var(--line-height);
padding: 1.5rem 0;
}
/* Scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: rgb(202, 202, 232) auto;
}
*::-webkit-scrollbar {
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: rgb(202, 202, 232);
border-radius: 10px;
}