mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
user: add a permission to disable changing api key authentication
also implement the missing APIs to enable/disable api key authentication
This commit is contained in:
parent
101c2962ab
commit
7bad65a43e
11 changed files with 479 additions and 8 deletions
|
@ -724,6 +724,11 @@ func (u *User) CanChangePassword() bool {
|
|||
return !util.IsStringInSlice(sdk.WebClientPasswordChangeDisabled, u.Filters.WebClient)
|
||||
}
|
||||
|
||||
// CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
|
||||
func (u *User) CanChangeAPIKeyAuth() bool {
|
||||
return !util.IsStringInSlice(sdk.WebClientAPIKeyAuthChangeDisabled, u.Filters.WebClient)
|
||||
}
|
||||
|
||||
// CanManagePublicKeys returns true if this user is allowed to manage public keys
|
||||
// from the web client. Used in web client UI
|
||||
func (u *User) CanManagePublicKeys() bool {
|
||||
|
|
|
@ -155,6 +155,50 @@ func deleteAdmin(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "Admin deleted", http.StatusOK)
|
||||
}
|
||||
|
||||
func getAdminAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
admin, err := dataprovider.AdminExists(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
resp := apiKeyAuth{
|
||||
AllowAPIKeyAuth: admin.Filters.AllowAPIKeyAuth,
|
||||
}
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
func changeAdminAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
admin, err := dataprovider.AdminExists(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
var req apiKeyAuth
|
||||
err = render.DecodeJSON(r.Body, &req)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
admin.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
|
||||
if err := dataprovider.UpdateAdmin(&admin); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, err, "API key authentication status updated", http.StatusOK)
|
||||
}
|
||||
|
||||
func changeAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
|
|
|
@ -349,6 +349,50 @@ func setUserPublicKeys(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "Public keys updated", http.StatusOK)
|
||||
}
|
||||
|
||||
func getUserAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
resp := apiKeyAuth{
|
||||
AllowAPIKeyAuth: user.Filters.AllowAPIKeyAuth,
|
||||
}
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
func changeUserAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req apiKeyAuth
|
||||
err = render.DecodeJSON(r.Body, &req)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
user.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
|
||||
if err := dataprovider.UpdateUser(&user); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, err, "API key authentication status updated", http.StatusOK)
|
||||
}
|
||||
|
||||
func changeUserPassword(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
|
|
|
@ -28,6 +28,10 @@ type pwdChange struct {
|
|||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type apiKeyAuth struct {
|
||||
AllowAPIKeyAuth bool `json:"allow_api_key_auth"`
|
||||
}
|
||||
|
||||
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||
var errorString string
|
||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
|
|
|
@ -55,6 +55,7 @@ const (
|
|||
adminPath = "/api/v2/admins"
|
||||
adminPwdPath = "/api/v2/admin/changepwd"
|
||||
adminPwdCompatPath = "/api/v2/changepwd/admin"
|
||||
adminManageAPIKeyPath = "/api/v2/admin/apikeyauth"
|
||||
userPwdPath = "/api/v2/user/changepwd"
|
||||
userPublicKeysPath = "/api/v2/user/publickeys"
|
||||
userFolderPath = "/api/v2/user/folder"
|
||||
|
@ -73,6 +74,7 @@ const (
|
|||
userTOTPValidatePath = "/api/v2/user/totp/validate"
|
||||
userTOTPSavePath = "/api/v2/user/totp/save"
|
||||
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
|
||||
userManageAPIKeyPath = "/api/v2/user/apikeyauth"
|
||||
healthzPath = "/healthz"
|
||||
webRootPathDefault = "/"
|
||||
webBasePathDefault = "/web"
|
||||
|
|
|
@ -92,11 +92,13 @@ const (
|
|||
adminTOTPValidatePath = "/api/v2/admin/totp/validate"
|
||||
adminTOTPSavePath = "/api/v2/admin/totp/save"
|
||||
admin2FARecoveryCodesPath = "/api/v2/admin/2fa/recoverycodes"
|
||||
adminManageAPIKeyPath = "/api/v2/admin/apikeyauth"
|
||||
userTOTPConfigsPath = "/api/v2/user/totp/configs"
|
||||
userTOTPGeneratePath = "/api/v2/user/totp/generate"
|
||||
userTOTPValidatePath = "/api/v2/user/totp/validate"
|
||||
userTOTPSavePath = "/api/v2/user/totp/save"
|
||||
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
|
||||
userManageAPIKeyPath = "/api/v2/user/apikeyauth"
|
||||
healthzPath = "/healthz"
|
||||
webBasePath = "/web"
|
||||
webBasePathAdmin = "/web/admin"
|
||||
|
@ -3334,6 +3336,17 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "the following characters are allowed")
|
||||
|
||||
apiKeyAuthReq := make(map[string]bool)
|
||||
apiKeyAuthReq["allow_api_key_auth"] = true
|
||||
asJSON, err := json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, userAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "the following characters are allowed")
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
|
@ -3352,6 +3365,17 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "the following characters are allowed")
|
||||
|
||||
apiKeyAuthReq = make(map[string]bool)
|
||||
apiKeyAuthReq["allow_api_key_auth"] = true
|
||||
asJSON, err = json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, adminAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "the following characters are allowed")
|
||||
|
||||
req, err = http.NewRequest(http.MethodPut, adminPath+"/"+admin.Username+"/2fa/disable", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, adminAPIToken)
|
||||
|
@ -5816,6 +5840,110 @@ func TestWebUserTOTP(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestWebAPIChangeUserAPIKeyAuth(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, user.Filters.AllowAPIKeyAuth)
|
||||
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
// invalid json
|
||||
req, err := http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer([]byte("{")))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
apiKeyAuthReq := make(map[string]bool)
|
||||
apiKeyAuthReq["allow_api_key_auth"] = true
|
||||
asJSON, err := json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, user.Filters.AllowAPIKeyAuth)
|
||||
|
||||
apiKeyAuthReq = make(map[string]bool)
|
||||
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, apiKeyAuthReq["allow_api_key_auth"])
|
||||
|
||||
apiKeyAuthReq["allow_api_key_auth"] = false
|
||||
asJSON, err = json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, user.Filters.AllowAPIKeyAuth)
|
||||
|
||||
apiKeyAuthReq = make(map[string]bool)
|
||||
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, apiKeyAuthReq["allow_api_key_auth"])
|
||||
|
||||
// remove the permission
|
||||
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, user.Filters.WebClient, 1)
|
||||
assert.Contains(t, user.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
|
||||
|
||||
newToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
apiKeyAuthReq["allow_api_key_auth"] = true
|
||||
asJSON, err = json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, newToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
// get will still work
|
||||
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, newToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
apiKeyAuthReq = make(map[string]bool)
|
||||
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestWebAPIChangeUserPwdMock(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -5890,6 +6018,82 @@ func TestLoginInvalidPasswordMock(t *testing.T) {
|
|||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
}
|
||||
|
||||
func TestChangeAdminAPIKeyAuth(t *testing.T) {
|
||||
admin := getTestAdmin()
|
||||
admin.Username = altAdminUsername
|
||||
admin.Password = altAdminPassword
|
||||
admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, admin.Filters.AllowAPIKeyAuth)
|
||||
|
||||
token, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)
|
||||
assert.NoError(t, err)
|
||||
// invalid json
|
||||
req, err := http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer([]byte("{")))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
apiKeyAuthReq := make(map[string]bool)
|
||||
apiKeyAuthReq["allow_api_key_auth"] = true
|
||||
asJSON, err := json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
admin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, admin.Filters.AllowAPIKeyAuth)
|
||||
|
||||
apiKeyAuthReq = make(map[string]bool)
|
||||
req, err = http.NewRequest(http.MethodGet, adminManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, apiKeyAuthReq["allow_api_key_auth"])
|
||||
|
||||
apiKeyAuthReq["allow_api_key_auth"] = false
|
||||
asJSON, err = json.Marshal(apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
apiKeyAuthReq = make(map[string]bool)
|
||||
req, err = http.NewRequest(http.MethodGet, adminManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, apiKeyAuthReq["allow_api_key_auth"])
|
||||
|
||||
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, adminManageAPIKeyPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestChangeAdminPwdMock(t *testing.T) {
|
||||
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
@ -9482,6 +9686,22 @@ func TestWebUserAllowAPIKeyAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.False(t, user.Filters.AllowAPIKeyAuth)
|
||||
|
||||
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, user.CanChangeAPIKeyAuth())
|
||||
|
||||
newToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
form = make(url.Values)
|
||||
form.Set("allow_api_key_auth", "1")
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, newToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
|
|
|
@ -410,6 +410,26 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
getUserAPIKeyAuthStatus(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
changeUserAPIKeyAuthStatus(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
getAdminAPIKeyAuthStatus(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
changeAdminAPIKeyAuthStatus(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
rr = httptest.NewRecorder()
|
||||
|
|
|
@ -242,6 +242,67 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/admin/apikeyauth:
|
||||
get:
|
||||
security:
|
||||
- BearerAuth: []
|
||||
tags:
|
||||
- admins
|
||||
summary: Get API key authentication status
|
||||
description: 'Returns the API Key authentication status for the logged in admin'
|
||||
operationId: get_admin_api_key_status
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
allow_api_key_auth:
|
||||
type: boolean
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
put:
|
||||
security:
|
||||
- BearerAuth: []
|
||||
tags:
|
||||
- admins
|
||||
summary: Update API key auth status
|
||||
description: 'Allows to enable/disable the API key authentication for the logged in admin. If enabled, you can impersonate this admin, in REST API, using an API key, otherwise your credentials, including two-factor authentication, if enabled, are required to use the REST API on your behalf'
|
||||
operationId: update_admin_api_key_status
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
allow_api_key_auth:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/admin/2fa/recoverycodes:
|
||||
get:
|
||||
security:
|
||||
|
@ -2263,6 +2324,67 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/apikeyauth:
|
||||
get:
|
||||
security:
|
||||
- BearerAuth: []
|
||||
tags:
|
||||
- users API
|
||||
summary: Get API key authentication status
|
||||
description: 'Returns the API Key authentication status for the logged in user'
|
||||
operationId: get_user_api_key_status
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
allow_api_key_auth:
|
||||
type: boolean
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
put:
|
||||
security:
|
||||
- BearerAuth: []
|
||||
tags:
|
||||
- users API
|
||||
summary: Update API key auth status
|
||||
description: 'Allows to enable/disable the API key authentication for the logged in user. If enabled, you can impersonate this user, in REST API, using an API key, otherwise your credentials, including two-factor authentication, if enabled, are required to use the REST API on your behalf'
|
||||
operationId: update_user_api_key_status
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
allow_api_key_auth:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/2fa/recoverycodes:
|
||||
get:
|
||||
security:
|
||||
|
@ -3026,12 +3148,14 @@ components:
|
|||
- write-disabled
|
||||
- mfa-disabled
|
||||
- password-change-disabled
|
||||
- api-key-auth-change-disabled
|
||||
description: |
|
||||
Options:
|
||||
* `publickey-change-disabled` - changing SSH public keys is not allowed
|
||||
* `write-disabled` - upload, rename, delete are not allowed even if the user has permissions for these actions
|
||||
* `mfa-disabled` - enabling multi-factor authentication is not allowed. This option cannot be set if the user has MFA already enabled
|
||||
* `password-change-disabled` - changing password is not allowed
|
||||
* `api-key-auth-change-disabled` - enabling/disabling API key authentication is not allowed
|
||||
APIKeyScope:
|
||||
type: integer
|
||||
enum:
|
||||
|
|
|
@ -905,6 +905,8 @@ func (s *httpdServer) initializeRouter() {
|
|||
})
|
||||
|
||||
router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
|
||||
router.With(forbidAPIKeyAuthentication).Get(adminManageAPIKeyPath, getAdminAPIKeyAuthStatus)
|
||||
router.With(forbidAPIKeyAuthentication).Put(adminManageAPIKeyPath, changeAdminAPIKeyAuthStatus)
|
||||
router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
|
||||
// compatibility layer to remove in v2.2
|
||||
router.With(forbidAPIKeyAuthentication).Put(adminPwdCompatPath, changeAdminPassword)
|
||||
|
@ -994,6 +996,9 @@ func (s *httpdServer) initializeRouter() {
|
|||
Get(userPublicKeysPath, getUserPublicKeys)
|
||||
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
|
||||
Put(userPublicKeysPath, setUserPublicKeys)
|
||||
router.With(forbidAPIKeyAuthentication).Get(userManageAPIKeyPath, getUserAPIKeyAuthStatus)
|
||||
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientAPIKeyAuthChangeDisabled)).
|
||||
Put(userManageAPIKeyPath, changeUserAPIKeyAuthStatus)
|
||||
// user TOTP APIs
|
||||
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
|
||||
Get(userTOTPConfigsPath, getTOTPConfigs)
|
||||
|
@ -1092,7 +1097,8 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
|
||||
Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
|
||||
router.Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientAPIKeyAuthChangeDisabled)).
|
||||
Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
|
||||
Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
|
||||
|
|
11
sdk/user.go
11
sdk/user.go
|
@ -9,16 +9,17 @@ import (
|
|||
|
||||
// Web Client/user REST API restrictions
|
||||
const (
|
||||
WebClientPubKeyChangeDisabled = "publickey-change-disabled"
|
||||
WebClientWriteDisabled = "write-disabled"
|
||||
WebClientMFADisabled = "mfa-disabled"
|
||||
WebClientPasswordChangeDisabled = "password-change-disabled"
|
||||
WebClientPubKeyChangeDisabled = "publickey-change-disabled"
|
||||
WebClientWriteDisabled = "write-disabled"
|
||||
WebClientMFADisabled = "mfa-disabled"
|
||||
WebClientPasswordChangeDisabled = "password-change-disabled"
|
||||
WebClientAPIKeyAuthChangeDisabled = "api-key-auth-change-disabled"
|
||||
)
|
||||
|
||||
var (
|
||||
// WebClientOptions defines the available options for the web client interface/user REST API
|
||||
WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled, WebClientMFADisabled,
|
||||
WebClientPasswordChangeDisabled}
|
||||
WebClientPasswordChangeDisabled, WebClientAPIKeyAuthChangeDisabled}
|
||||
// UserTypes defines the supported user type hints for auth plugins
|
||||
UserTypes = []string{string(UserTypeLDAP), string(UserTypeOS)}
|
||||
)
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
<form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
|
||||
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}}
|
||||
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
|
||||
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
|
||||
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
|
||||
|
@ -118,9 +118,10 @@
|
|||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .LoggedUser.CanChangeAPIKeyAuth}}
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue