Compare commits
284 Commits
ad58d84396
...
main
Author | SHA1 | Date | |
---|---|---|---|
2754fb8144 | |||
4650a971a3 | |||
8cd1292a41 | |||
ea5198a5b9 | |||
d000469836 | |||
02061f5b26 | |||
ea3e8ddfbc | |||
8182b899cf | |||
ae5b864feb | |||
4a6a7270f9 | |||
a7beb94341 | |||
4a0e98bab8 | |||
66a1917e6f | |||
526161f6b4 | |||
ff16acc816 | |||
ee822b5c9d | |||
e427184310 | |||
5719ce8f5d | |||
a78f2b7c88 | |||
1171a7bbaa | |||
8ff92e206e | |||
19ffc9e683 | |||
968bcf1b7a | |||
77d063867a | |||
c82bffe421 | |||
b801563074 | |||
4bc430633e | |||
69a25fbb09 | |||
840b9f4863 | |||
5b87ef0d30 | |||
bfc734a6d1 | |||
b5c9b5ce19 | |||
9f0dafd4fd | |||
abaa291a14 | |||
de1a076d64 | |||
1b5a2e89dd | |||
8799f0f796 | |||
317e0ab83d | |||
116a9e827b | |||
2ab6240a24 | |||
bb3bf3093d | |||
1d1aa098a9 | |||
f68bd9637d | |||
5f63ee235b | |||
092fe32baf | |||
44d3bc71ed | |||
dc3c5d1068 | |||
d834a5c362 | |||
9e0c1e7cd7 | |||
9ac729b684 | |||
7f43662cbc | |||
b35d365467 | |||
77d487c1ce | |||
f5827ef432 | |||
6e1a28d2df | |||
5c3b2e19cf | |||
e109cd084d | |||
f29c733080 | |||
97cd75b0d7 | |||
b278a3c7d8 | |||
498dd9a8c3 | |||
c99ffa8368 | |||
8fec84c118 | |||
2c63ea9632 | |||
12bfa69cce | |||
f39a06dfd6 | |||
65af8a7194 | |||
f6a2544839 | |||
e71c9e6df8 | |||
db63b89c61 | |||
95125b691d | |||
5920db48d8 | |||
1a297464fe | |||
083fb0ebe1 | |||
a3333cebb6 | |||
d087492c31 | |||
62606cbee5 | |||
b65d1ef52e | |||
e59b9eefc2 | |||
a85ed3fad8 | |||
bf1174bd0f | |||
17aa04c1ea | |||
91143f2628 | |||
1a90963851 | |||
afec665759 | |||
1ffa19d225 | |||
1bd832f839 | |||
eb5707a376 | |||
b77e47ba7e | |||
73c487fd3d | |||
f467304e9e | |||
06bbe010dd | |||
03cb298618 | |||
e14f4007a4 | |||
cef741f2d8 | |||
7b11958658 | |||
0273f62611 | |||
4b44c93693 | |||
50f078db7a | |||
8143470b5a | |||
b828416811 | |||
f8000b749e | |||
1c3307d43d | |||
eaeba09078 | |||
0899b07d47 | |||
48611b22c9 | |||
ed6966eab4 | |||
341d5961f0 | |||
32512f3c04 | |||
25510c63e5 | |||
fc736df4e3 | |||
80cb06a64c | |||
4a62b9db25 | |||
7e5be4e74f | |||
9c2ff979fb | |||
391d806c13 | |||
07ae9cf2ac | |||
1b1ac50a61 | |||
43aabee7e8 | |||
5a87ecebfc | |||
203a564b04 | |||
b4355ee913 | |||
d86ce64ea7 | |||
dc9ffceb3e | |||
1bb983b4b9 | |||
d8b68f2815 | |||
43e0ecd1ce | |||
e31c6e5c78 | |||
e57bfbdffe | |||
c131674227 | |||
a7d839e712 | |||
6bd65d1261 | |||
07f5f6ee83 | |||
b5a28c0d39 | |||
f7414629bc | |||
3c8d18afc4 | |||
a8b645288b | |||
54a8c1504e | |||
22e039e035 | |||
77979b1839 | |||
db26c12483 | |||
123925e304 | |||
b57a4ed95c | |||
13d5df2953 | |||
00f87ccb71 | |||
43fa0b02aa | |||
6423d83949 | |||
90da2367be | |||
92dcd67381 | |||
f45a0f3aeb | |||
bc1ca7b481 | |||
1c3d82ffba | |||
3186fe9ebc | |||
a4a25164d0 | |||
bc1021cd12 | |||
4b62b75c78 | |||
89bd1acc32 | |||
17fc1f2e66 | |||
5534347be7 | |||
e5764553d8 | |||
143e402dd6 | |||
309d9c8fa3 | |||
c80ccd47bf | |||
bc3591a690 | |||
07fd43bf33 | |||
7363936cd5 | |||
1d9240456d | |||
e89182c2d9 | |||
dbc2276d68 | |||
20dc745a64 | |||
598b9442c0 | |||
c9a161ca25 | |||
8cf05b6858 | |||
affe9021aa | |||
46567ab1a4 | |||
fdfde7af0b | |||
1a2b6e5b41 | |||
ac60d1daef | |||
d1857f2db1 | |||
0c232178db | |||
586f275c91 | |||
fe502e150f | |||
85f96a31d2 | |||
fd626a9cbe | |||
d1eecc5c4f | |||
7ecf27f7dc | |||
c181095197 | |||
20b38677f4 | |||
d5c50c146f | |||
9a107bc4ba | |||
d8da3027e2 | |||
71c5de3762 | |||
04bf8270bb | |||
aba655cd3b | |||
d45e61f59e | |||
cdbf490b68 | |||
675744ad74 | |||
38076b6126 | |||
e82ce9009f | |||
7bc20231ec | |||
50b512e08e | |||
7f40884115 | |||
63cfe1fd8d | |||
b51468db8c | |||
cb7376eeeb | |||
fa4f896093 | |||
5c3f31224a | |||
4409f8e2ff | |||
217eebead8 | |||
8068ddc0b2 | |||
ffa8778d2b | |||
44b92a8b08 | |||
3d47ccd746 | |||
ad93b59769 | |||
f57f11d855 | |||
7a8fd8e200 | |||
d6c082675e | |||
ead1340659 | |||
25414aada0 | |||
3feda1d102 | |||
f5fc5c0a56 | |||
2b331719b9 | |||
4e0e55473a | |||
5a70d0f27a | |||
42e15c7176 | |||
53e48a50ac | |||
5ded1b3696 | |||
5e52813111 | |||
023fdc22a6 | |||
ea70e073ec | |||
55f7bacd7b | |||
2398288e08 | |||
f0e9751563 | |||
e8abd27f3c | |||
0619b497f7 | |||
f7168d465a | |||
85bea202f0 | |||
e7b2c86ba7 | |||
f6602f2823 | |||
35583b5f86 | |||
ec8abc0802 | |||
396d0c2f00 | |||
dfe0c28094 | |||
4d56452f26 | |||
1ace119b02 | |||
ba02b56971 | |||
33a5c3dff2 | |||
23af62ef44 | |||
9983d3e8db | |||
51c5461632 | |||
3801122360 | |||
3e54351477 | |||
dbcdf56818 | |||
3876302bbd | |||
fe2fefbe86 | |||
83c126d853 | |||
c00f01b084 | |||
503bcfbd8d | |||
8d2f226090 | |||
a7f0bdf09b | |||
fb2dce5414 | |||
6baa0fe103 | |||
e20e799808 | |||
1e4fd9d5d6 | |||
6f47262336 | |||
ca39234f12 | |||
484acd1822 | |||
9203e09d2d | |||
ca316e7086 | |||
95c6bccefb | |||
969d1ca8d0 | |||
b9a0c3ec0a | |||
70f8103901 | |||
1f80d0b9ad | |||
9d15d20a1b | |||
05d7af20c1 | |||
bf235cdebe | |||
747487b764 | |||
45ceae73c4 | |||
61f2813802 | |||
93385646b6 | |||
2d7a55e427 | |||
1c419454a2 | |||
ab60f8796a |
@@ -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"
|
||||
|
128
.drone.yml
128
.drone.yml
@@ -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
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
api\ tests.txt
|
||||
ccsecrets
|
||||
ccsecrets.*
|
||||
smt
|
||||
smt.*
|
||||
.env
|
||||
*.pem
|
||||
.DS_Store
|
483
README.md
483
README.md
@@ -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
|
||||

|
619
api-test.sh
Normal file
619
api-test.sh
Normal 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
|
@@ -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})
|
||||
|
||||
}
|
||||
|
196
controllers/controlGroups.go
Normal file
196
controllers/controlGroups.go
Normal 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"})
|
||||
|
||||
}
|
267
controllers/controlPermissions.go
Normal file
267
controllers/controlPermissions.go
Normal 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(¤tPermission, &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
137
controllers/controlSafes.go
Normal 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"})
|
||||
}
|
||||
}
|
23
controllers/retrieveAudits.go
Normal file
23
controllers/retrieveAudits.go
Normal 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})
|
||||
}
|
219
controllers/retrieveSecrets.go
Normal file
219
controllers/retrieveSecrets.go
Normal 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)})
|
||||
|
||||
}
|
@@ -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
502
controllers/storeSecrets.go
Normal 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
|
||||
}
|
||||
*/
|
||||
}
|
@@ -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
45
controllers/unlock.go
Normal 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
77
go.mod
@@ -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
262
go.sum
@@ -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
254
index.htm
Normal 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>{
|
||||
"secretKey": "Example32ByteSecretKey0123456789"
|
||||
}
|
||||
</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>{
|
||||
"username": "",
|
||||
"password": "",
|
||||
"RoleId": 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>{
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
</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>"Authorization: Bearer <JWT_TOKEN>"</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>{
|
||||
"deviceName": "",
|
||||
"deviceCategory": "",
|
||||
"userName": "",
|
||||
"secretValue": ""
|
||||
}
|
||||
</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>{
|
||||
"deviceName": "",
|
||||
"deviceCategory": "",
|
||||
"userName": ""
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>Must be logged in to execute this command. Only secrets registered with the current user’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/<searchname></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’s RoleId can be retrieved.</p>
|
||||
|
||||
<p>GET <code>/api/secret/retrieve/category/<searchname></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’s RoleId can be retrieved.</p>
|
||||
|
||||
<h4>Update</h4>
|
||||
|
||||
<p>POST <code>/api/secret/update</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"deviceName": "",
|
||||
"deviceCategory": "",
|
||||
"userName": "",
|
||||
"secretValue": ""
|
||||
}
|
||||
</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
187
main.go
@@ -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
|
||||
|
@@ -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
68
models/audit.go
Normal 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
546
models/db.go
Normal 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
143
models/group.go
Normal 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
119
models/key.go
Normal 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
384
models/ldap.go
Normal 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
170
models/permission.go
Normal 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
|
||||
}
|
@@ -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
83
models/role.go.txt
Normal 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
152
models/safe.go
Normal 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
|
||||
}
|
370
models/secret.go
370
models/secret.go
@@ -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 {
|
||||
|
246
models/setup.go
246
models/setup.go
@@ -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
|
||||
}
|
438
models/user.go
438
models/user.go
@@ -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 (u *User) PrepareGive() {
|
||||
u.Password = ""
|
||||
}
|
||||
*/
|
||||
|
||||
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)
|
||||
|
||||
/*
|
||||
// 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
84
template.html
Normal 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 ↗</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
12
test.env
Normal 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
139
utils/certOperations.go
Normal 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
121
utils/structOperations.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
131
utils/utils.go
131
utils/utils.go
@@ -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
BIN
www/database.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
BIN
www/favicon-16x16.png
Normal file
BIN
www/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 637 B |
BIN
www/favicon-32x32.png
Normal file
BIN
www/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
www/favicon.ico
Normal file
BIN
www/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
536
www/mvp.css
Normal file
536
www/mvp.css
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user