Skip to content

Commit 26b6537

Browse files
committed
Only display redacted versions of API keys #228, fixed deployment password
1 parent 366f968 commit 26b6537

File tree

13 files changed

+144
-70
lines changed

13 files changed

+144
-70
lines changed

internal/configuration/Configuration.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func MigrateToV2(authPassword string) {
129129

130130
for _, apiKey := range database.GetAllApiKeys() {
131131
apiKey.UserId = user.Id
132+
apiKey.PublicId = helper.GenerateRandomString(35)
132133
database.SaveApiKey(apiKey)
133134
}
134135

@@ -227,7 +228,9 @@ func SetDeploymentPassword(newPassword string) {
227228
os.Exit(1)
228229
}
229230
serverSettings.Authentication.SaltAdmin = helper.GenerateRandomString(30)
230-
err := database.EditSuperAdmin(serverSettings.Authentication.Username, serverSettings.Authentication.Username, newPassword)
231+
err := database.EditSuperAdmin(serverSettings.Authentication.Username,
232+
serverSettings.Authentication.Username,
233+
hashUserPassword(newPassword))
231234
if err != nil {
232235
fmt.Println("No super-admin user found, but database contains other users. Aborting.")
233236
os.Exit(1)

internal/configuration/database/Database.go

+5
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ func GetSystemKey(userId int) (models.ApiKey, bool) {
135135
return db.GetSystemKey(userId)
136136
}
137137

138+
// GetApiKeyByPublicKey returns an API key by using the public key
139+
func GetApiKeyByPublicKey(publicKey string) (string, bool) {
140+
return db.GetApiKeyByPublicKey(publicKey)
141+
}
142+
138143
// E2E Section
139144

140145
// SaveEnd2EndInfo stores the encrypted e2e info

internal/configuration/database/dbabstraction/DbAbstraction.go

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type Database interface {
4545
DeleteApiKey(id string)
4646
// GetSystemKey returns the latest UI API key
4747
GetSystemKey(userId int) (models.ApiKey, bool)
48+
// GetApiKeyByPublicKey returns an API key by using the public key
49+
GetApiKeyByPublicKey(publicKey string) (string, bool)
4850

4951
// SaveEnd2EndInfo stores the encrypted e2e info
5052
SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int)

internal/configuration/database/provider/redis/apikeys.go

+11
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) {
6464
return keys[foundKey], true
6565
}
6666

67+
// GetApiKeyByPublicKey returns an API key by using the public key
68+
func (p DatabaseProvider) GetApiKeyByPublicKey(publicKey string) (string, bool) {
69+
keys := p.GetAllApiKeys()
70+
for _, key := range keys {
71+
if key.PublicId == publicKey {
72+
return key.Id, true
73+
}
74+
}
75+
return "", false
76+
}
77+
6778
// SaveApiKey saves the API key to the database
6879
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
6980
p.setHashMap(p.buildArgs(prefixApiKeys + apikey.Id).AddFlat(apikey))

internal/configuration/database/provider/sqlite/Sqlite.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
3838
}
3939
// < v2.0.0-beta
4040
if currentDbVersion < 7 {
41-
err := p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN UserId INTEGER NOT NULL DEFAULT 0;`)
41+
err := p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN UserId INTEGER NOT NULL DEFAULT 0;
42+
ALTER TABLE "ApiKeys" ADD COLUMN PublicId TEXT NOT NULL DEFAULT '';`)
4243
helper.Check(err)
4344
err = p.rawSqlite(`DELETE FROM "ApiKeys" WHERE IsSystemKey = 1`)
4445
helper.Check(err)
@@ -145,6 +146,7 @@ func (p DatabaseProvider) createNewDatabase() error {
145146
"Expiry" INTEGER,
146147
"IsSystemKey" INTEGER,
147148
"UserId" INTEGER NOT NULL,
149+
"PublicId" TEXT NOT NULL UNIQUE ,
148150
PRIMARY KEY("Id")
149151
) WITHOUT ROWID;
150152
CREATE TABLE "E2EConfig" (

internal/configuration/database/provider/sqlite/apikeys.go

+24-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type schemaApiKeys struct {
1616
Expiry int64
1717
IsSystemKey int
1818
UserId int
19+
PublicId string
1920
}
2021

2122
// currentTime is used in order to modify the current time for testing purposes in unit tests
@@ -32,10 +33,11 @@ func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
3233
defer rows.Close()
3334
for rows.Next() {
3435
rowData := schemaApiKeys{}
35-
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions, &rowData.Expiry, &rowData.IsSystemKey, &rowData.UserId)
36+
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions, &rowData.Expiry, &rowData.IsSystemKey, &rowData.UserId, &rowData.PublicId)
3637
helper.Check(err)
3738
result[rowData.Id] = models.ApiKey{
3839
Id: rowData.Id,
40+
PublicId: rowData.PublicId,
3941
FriendlyName: rowData.FriendlyName,
4042
LastUsed: rowData.LastUsed,
4143
Permissions: uint8(rowData.Permissions),
@@ -51,7 +53,7 @@ func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
5153
func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
5254
var rowResult schemaApiKeys
5355
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE Id = ?", id)
54-
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey, &rowResult.UserId)
56+
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey, &rowResult.UserId, &rowResult.PublicId)
5557
if err != nil {
5658
if errors.Is(err, sql.ErrNoRows) {
5759
return models.ApiKey{}, false
@@ -62,6 +64,7 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
6264

6365
result := models.ApiKey{
6466
Id: rowResult.Id,
67+
PublicId: rowResult.PublicId,
6568
FriendlyName: rowResult.FriendlyName,
6669
LastUsed: rowResult.LastUsed,
6770
Permissions: uint8(rowResult.Permissions),
@@ -77,7 +80,7 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
7780
func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) {
7881
var rowResult schemaApiKeys
7982
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE IsSystemKey = 1 AND UserId = ? ORDER BY Expiry DESC LIMIT 1", userId)
80-
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey, &rowResult.UserId)
83+
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey, &rowResult.UserId, &rowResult.PublicId)
8184
if err != nil {
8285
if errors.Is(err, sql.ErrNoRows) {
8386
return models.ApiKey{}, false
@@ -88,6 +91,7 @@ func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) {
8891

8992
result := models.ApiKey{
9093
Id: rowResult.Id,
94+
PublicId: rowResult.PublicId,
9195
FriendlyName: rowResult.FriendlyName,
9296
LastUsed: rowResult.LastUsed,
9397
Permissions: uint8(rowResult.Permissions),
@@ -98,14 +102,29 @@ func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) {
98102
return result, true
99103
}
100104

105+
// GetApiKeyByPublicKey returns an API key by using the public key
106+
func (p DatabaseProvider) GetApiKeyByPublicKey(publicKey string) (string, bool) {
107+
var rowResult schemaApiKeys
108+
row := p.sqliteDb.QueryRow("SELECT Id FROM ApiKeys WHERE PublicId = ? LIMIT 1", publicKey)
109+
err := row.Scan(&rowResult.Id)
110+
if err != nil {
111+
if errors.Is(err, sql.ErrNoRows) {
112+
return "", false
113+
}
114+
helper.Check(err)
115+
return "", false
116+
}
117+
return rowResult.Id, true
118+
}
119+
101120
// SaveApiKey saves the API key to the database
102121
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
103122
isSystemKey := 0
104123
if apikey.IsSystemKey {
105124
isSystemKey = 1
106125
}
107-
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions, Expiry, IsSystemKey, UserId) VALUES (?, ?, ?, ?, ?, ?, ?)",
108-
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions, apikey.Expiry, isSystemKey, apikey.UserId)
126+
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions, Expiry, IsSystemKey, UserId, PublicId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
127+
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions, apikey.Expiry, isSystemKey, apikey.UserId, apikey.PublicId)
109128
helper.Check(err)
110129
}
111130

internal/models/ApiKey.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const ApiPermDefault = ApiPermAll - ApiPermApiMod - ApiPermManageUsers - ApiPerm
3232
// ApiKey contains data of a single api key
3333
type ApiKey struct {
3434
Id string `json:"Id" redis:"Id"`
35+
PublicId string `json:"PublicId" redis:"PublicId"`
3536
FriendlyName string `json:"FriendlyName" redis:"FriendlyName"`
3637
LastUsed int64 `json:"LastUsed" redis:"LastUsed"`
3738
Permissions uint8 `json:"Permissions" redis:"Permissions"`
@@ -48,6 +49,12 @@ func (key *ApiKey) GetReadableDate() string {
4849
return time.Unix(key.LastUsed, 0).Format("2006-01-02 15:04:05")
4950
}
5051

52+
// GetRedactedId returns a redacted version of the API key
53+
func (key *ApiKey) GetRedactedId() string {
54+
return key.Id[0:2] + "**************************" + key.Id[28:]
55+
56+
}
57+
5158
// GrantPermission sets one or more permissions
5259
func (key *ApiKey) GrantPermission(permission uint8) {
5360
key.Permissions |= permission
@@ -103,6 +110,7 @@ func (key *ApiKey) HasPermissionManageUsers() bool {
103110

104111
// ApiKeyOutput is the output that is used after a new key is created
105112
type ApiKeyOutput struct {
106-
Result string
107-
Id string
113+
Result string
114+
Id string
115+
PublicId string
108116
}

internal/webserver/api/Api.go

+26-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import (
1717
"time"
1818
)
1919

20+
const lengthPublicId = 35
21+
const lengthApiKey = 30
22+
2023
// Process parses the request and executes the API call or returns an error message to the sender
2124
func Process(w http.ResponseWriter, r *http.Request, maxMemory int) {
2225
w.Header().Set("cache-control", "no-store")
@@ -180,21 +183,20 @@ func DeleteKey(id string) bool {
180183
}
181184

182185
// NewKey generates a new API key
183-
func NewKey(defaultPermissions bool, userId int) string {
186+
func NewKey(defaultPermissions bool, userId int) models.ApiKey {
184187
newKey := models.ApiKey{
185-
Id: helper.GenerateRandomString(30),
188+
Id: helper.GenerateRandomString(lengthApiKey),
189+
PublicId: helper.GenerateRandomString(lengthPublicId),
186190
FriendlyName: "Unnamed key",
187-
LastUsed: 0,
188191
Permissions: models.ApiPermDefault,
189-
Expiry: 0,
190192
IsSystemKey: false,
191193
UserId: userId,
192194
}
193195
if !defaultPermissions {
194196
newKey.Permissions = models.ApiPermNone
195197
}
196198
database.SaveApiKey(newKey)
197-
return newKey.Id
199+
return newKey
198200
}
199201

200202
// newSystemKey generates a new API key that is only used internally for the GUI
@@ -215,9 +217,9 @@ func newSystemKey(userId int) string {
215217
}
216218

217219
newKey := models.ApiKey{
218-
Id: helper.GenerateRandomString(30),
220+
Id: helper.GenerateRandomString(lengthApiKey),
221+
PublicId: helper.GenerateRandomString(lengthPublicId),
219222
FriendlyName: "Internal System Key",
220-
LastUsed: 0,
221223
Permissions: tempKey.Permissions,
222224
Expiry: time.Now().Add(time.Hour * 48).Unix(),
223225
IsSystemKey: true,
@@ -320,11 +322,12 @@ func isValidUserForEditing(w http.ResponseWriter, request apiRequest) (models.Us
320322
func createApiKey(w http.ResponseWriter, request apiRequest, user models.User) {
321323
key := NewKey(request.apiInfo.basicPermissions, user.Id)
322324
output := models.ApiKeyOutput{
323-
Result: "OK",
324-
Id: key,
325+
Result: "OK",
326+
Id: key.Id,
327+
PublicId: key.PublicId,
325328
}
326329
if request.apiInfo.friendlyName != "" {
327-
err := renameApiKeyFriendlyName(key, request.apiInfo.friendlyName)
330+
err := renameApiKeyFriendlyName(key.Id, request.apiInfo.friendlyName)
328331
if err != nil {
329332
sendError(w, http.StatusInternalServerError, err.Error())
330333
return
@@ -848,7 +851,7 @@ func parseRequest(r *http.Request) (apiRequest, error) {
848851
},
849852
apiInfo: apiModInfo{
850853
friendlyName: r.Header.Get("friendlyName"),
851-
apiKeyToModify: r.Header.Get("apiKeyToModify"),
854+
apiKeyToModify: publicKeyToApiKey(r.Header.Get("apiKeyToModify")),
852855
permission: uint8(apiPermission),
853856
grantPermission: r.Header.Get("permissionModifier") == "GRANT",
854857
basicPermissions: r.Header.Get("basicPermissions") == "true",
@@ -866,6 +869,18 @@ func parseRequest(r *http.Request) (apiRequest, error) {
866869
}, nil
867870
}
868871

872+
// publicKeyToApiKey tries to convert a (possible) public key to a private key
873+
// If not a public key or if invalid, the original value is returned
874+
func publicKeyToApiKey(publicKey string) string {
875+
if len(publicKey) == lengthPublicId {
876+
privateApiKey, ok := database.GetApiKeyByPublicKey(publicKey)
877+
if ok {
878+
return privateApiKey
879+
}
880+
}
881+
return publicKey
882+
}
883+
869884
func apiRequestToUploadRequest(request *http.Request) (models.UploadRequest, int, string, error) {
870885
paramsToChange := 0
871886
allowedDownloads := 0

internal/webserver/web/static/apidocumentation/openapi.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@
548548
{
549549
"name": "apiKeyToModify",
550550
"in": "header",
551-
"description": "The API key to change the name of",
551+
"description": "The API key to change the name of. Can be either the public ID or the actual API key",
552552
"required": true,
553553
"style": "simple",
554554
"explode": false,
@@ -598,7 +598,7 @@
598598
{
599599
"name": "apiKeyToModify",
600600
"in": "header",
601-
"description": "The API key to change the permission of",
601+
"description": "The API key to change the permission of. Can be either the public ID or the actual API key",
602602
"required": true,
603603
"style": "simple",
604604
"explode": false,
@@ -664,7 +664,7 @@
664664
{
665665
"name": "apiKeyToModify",
666666
"in": "header",
667-
"description": "The API key to delete",
667+
"description": "The API key to delete. Can be either the public ID or the actual API key",
668668
"required": true,
669669
"style": "simple",
670670
"explode": false,
@@ -1064,6 +1064,10 @@
10641064
"Id": {
10651065
"type": "string",
10661066
"example": "ar3iecahghiethiemeeR"
1067+
},
1068+
"PublicId": {
1069+
"type": "string",
1070+
"example": "oepah5iesae8YeeZohrain5ahNgax8su"
10671071
}
10681072
},
10691073
"description": "NewApiKey is the struct used for the result after creating a new API key",

0 commit comments

Comments
 (0)