Bläddra i källkod

user: add a permission to disable changing api key authentication

also implement the missing APIs to enable/disable api key authentication
Nicola Murino 3 år sedan
förälder
incheckning
7bad65a43e

+ 5 - 0
dataprovider/user.go

@@ -724,6 +724,11 @@ func (u *User) CanChangePassword() bool {
 	return !util.IsStringInSlice(sdk.WebClientPasswordChangeDisabled, u.Filters.WebClient)
 	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
 // CanManagePublicKeys returns true if this user is allowed to manage public keys
 // from the web client. Used in web client UI
 // from the web client. Used in web client UI
 func (u *User) CanManagePublicKeys() bool {
 func (u *User) CanManagePublicKeys() bool {

+ 44 - 0
httpd/api_admin.go

@@ -155,6 +155,50 @@ func deleteAdmin(w http.ResponseWriter, r *http.Request) {
 	sendAPIResponse(w, r, err, "Admin deleted", http.StatusOK)
 	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) {
 func changeAdminPassword(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
 

+ 44 - 0
httpd/api_http_user.go

@@ -349,6 +349,50 @@ func setUserPublicKeys(w http.ResponseWriter, r *http.Request) {
 	sendAPIResponse(w, r, err, "Public keys updated", http.StatusOK)
 	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) {
 func changeUserPassword(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
 

+ 4 - 0
httpd/api_utils.go

@@ -28,6 +28,10 @@ type pwdChange struct {
 	NewPassword     string `json:"new_password"`
 	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) {
 func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
 	var errorString string
 	var errorString string
 	if _, ok := err.(*util.RecordNotFoundError); ok {
 	if _, ok := err.(*util.RecordNotFoundError); ok {

+ 2 - 0
httpd/httpd.go

@@ -55,6 +55,7 @@ const (
 	adminPath                              = "/api/v2/admins"
 	adminPath                              = "/api/v2/admins"
 	adminPwdPath                           = "/api/v2/admin/changepwd"
 	adminPwdPath                           = "/api/v2/admin/changepwd"
 	adminPwdCompatPath                     = "/api/v2/changepwd/admin"
 	adminPwdCompatPath                     = "/api/v2/changepwd/admin"
+	adminManageAPIKeyPath                  = "/api/v2/admin/apikeyauth"
 	userPwdPath                            = "/api/v2/user/changepwd"
 	userPwdPath                            = "/api/v2/user/changepwd"
 	userPublicKeysPath                     = "/api/v2/user/publickeys"
 	userPublicKeysPath                     = "/api/v2/user/publickeys"
 	userFolderPath                         = "/api/v2/user/folder"
 	userFolderPath                         = "/api/v2/user/folder"
@@ -73,6 +74,7 @@ const (
 	userTOTPValidatePath                   = "/api/v2/user/totp/validate"
 	userTOTPValidatePath                   = "/api/v2/user/totp/validate"
 	userTOTPSavePath                       = "/api/v2/user/totp/save"
 	userTOTPSavePath                       = "/api/v2/user/totp/save"
 	user2FARecoveryCodesPath               = "/api/v2/user/2fa/recoverycodes"
 	user2FARecoveryCodesPath               = "/api/v2/user/2fa/recoverycodes"
+	userManageAPIKeyPath                   = "/api/v2/user/apikeyauth"
 	healthzPath                            = "/healthz"
 	healthzPath                            = "/healthz"
 	webRootPathDefault                     = "/"
 	webRootPathDefault                     = "/"
 	webBasePathDefault                     = "/web"
 	webBasePathDefault                     = "/web"

+ 220 - 0
httpd/httpd_test.go

@@ -92,11 +92,13 @@ const (
 	adminTOTPValidatePath           = "/api/v2/admin/totp/validate"
 	adminTOTPValidatePath           = "/api/v2/admin/totp/validate"
 	adminTOTPSavePath               = "/api/v2/admin/totp/save"
 	adminTOTPSavePath               = "/api/v2/admin/totp/save"
 	admin2FARecoveryCodesPath       = "/api/v2/admin/2fa/recoverycodes"
 	admin2FARecoveryCodesPath       = "/api/v2/admin/2fa/recoverycodes"
+	adminManageAPIKeyPath           = "/api/v2/admin/apikeyauth"
 	userTOTPConfigsPath             = "/api/v2/user/totp/configs"
 	userTOTPConfigsPath             = "/api/v2/user/totp/configs"
 	userTOTPGeneratePath            = "/api/v2/user/totp/generate"
 	userTOTPGeneratePath            = "/api/v2/user/totp/generate"
 	userTOTPValidatePath            = "/api/v2/user/totp/validate"
 	userTOTPValidatePath            = "/api/v2/user/totp/validate"
 	userTOTPSavePath                = "/api/v2/user/totp/save"
 	userTOTPSavePath                = "/api/v2/user/totp/save"
 	user2FARecoveryCodesPath        = "/api/v2/user/2fa/recoverycodes"
 	user2FARecoveryCodesPath        = "/api/v2/user/2fa/recoverycodes"
+	userManageAPIKeyPath            = "/api/v2/user/apikeyauth"
 	healthzPath                     = "/healthz"
 	healthzPath                     = "/healthz"
 	webBasePath                     = "/web"
 	webBasePath                     = "/web"
 	webBasePathAdmin                = "/web/admin"
 	webBasePathAdmin                = "/web/admin"
@@ -3334,6 +3336,17 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
 	checkResponseCode(t, http.StatusBadRequest, rr)
 	checkResponseCode(t, http.StatusBadRequest, rr)
 	assert.Contains(t, rr.Body.String(), "the following characters are allowed")
 	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)
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	err = os.RemoveAll(user.GetHomeDir())
 	err = os.RemoveAll(user.GetHomeDir())
@@ -3352,6 +3365,17 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
 	checkResponseCode(t, http.StatusOK, rr)
 	checkResponseCode(t, http.StatusOK, rr)
 	assert.Contains(t, rr.Body.String(), "the following characters are allowed")
 	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)
 	req, err = http.NewRequest(http.MethodPut, adminPath+"/"+admin.Username+"/2fa/disable", nil)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	setBearerForReq(req, adminAPIToken)
 	setBearerForReq(req, adminAPIToken)
@@ -5816,6 +5840,110 @@ func TestWebUserTOTP(t *testing.T) {
 	checkResponseCode(t, http.StatusNotFound, rr)
 	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) {
 func TestWebAPIChangeUserPwdMock(t *testing.T) {
 	user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
 	user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -5890,6 +6018,82 @@ func TestLoginInvalidPasswordMock(t *testing.T) {
 	assert.Equal(t, http.StatusUnauthorized, rr.Code)
 	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) {
 func TestChangeAdminPwdMock(t *testing.T) {
 	token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
 	token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -9482,6 +9686,22 @@ func TestWebUserAllowAPIKeyAuth(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.False(t, user.Filters.AllowAPIKeyAuth)
 	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())
 	err = os.RemoveAll(user.GetHomeDir())
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)

+ 20 - 0
httpd/internal_test.go

@@ -410,6 +410,26 @@ func TestInvalidToken(t *testing.T) {
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	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 := httpdServer{}
 	server.initializeRouter()
 	server.initializeRouter()
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()

+ 124 - 0
httpd/schema/openapi.yaml

@@ -242,6 +242,67 @@ paths:
           $ref: '#/components/responses/InternalServerError'
           $ref: '#/components/responses/InternalServerError'
         default:
         default:
           $ref: '#/components/responses/DefaultResponse'
           $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:
   /admin/2fa/recoverycodes:
     get:
     get:
       security:
       security:
@@ -2263,6 +2324,67 @@ paths:
           $ref: '#/components/responses/InternalServerError'
           $ref: '#/components/responses/InternalServerError'
         default:
         default:
           $ref: '#/components/responses/DefaultResponse'
           $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:
   /user/2fa/recoverycodes:
     get:
     get:
       security:
       security:
@@ -3026,12 +3148,14 @@ components:
         - write-disabled
         - write-disabled
         - mfa-disabled
         - mfa-disabled
         - password-change-disabled
         - password-change-disabled
+        - api-key-auth-change-disabled
       description: |
       description: |
         Options:
         Options:
           * `publickey-change-disabled` - changing SSH public keys is not allowed
           * `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
           * `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
           * `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
           * `password-change-disabled` - changing password is not allowed
+          * `api-key-auth-change-disabled` - enabling/disabling API key authentication is not allowed
     APIKeyScope:
     APIKeyScope:
       type: integer
       type: integer
       enum:
       enum:

+ 7 - 1
httpd/server.go

@@ -905,6 +905,8 @@ func (s *httpdServer) initializeRouter() {
 		})
 		})
 
 
 		router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
 		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)
 		router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
 		// compatibility layer to remove in v2.2
 		// compatibility layer to remove in v2.2
 		router.With(forbidAPIKeyAuthentication).Put(adminPwdCompatPath, changeAdminPassword)
 		router.With(forbidAPIKeyAuthentication).Put(adminPwdCompatPath, changeAdminPassword)
@@ -994,6 +996,9 @@ func (s *httpdServer) initializeRouter() {
 			Get(userPublicKeysPath, getUserPublicKeys)
 			Get(userPublicKeysPath, getUserPublicKeys)
 		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
 		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
 			Put(userPublicKeysPath, setUserPublicKeys)
 			Put(userPublicKeysPath, setUserPublicKeys)
+		router.With(forbidAPIKeyAuthentication).Get(userManageAPIKeyPath, getUserAPIKeyAuthStatus)
+		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientAPIKeyAuthChangeDisabled)).
+			Put(userManageAPIKeyPath, changeUserAPIKeyAuthStatus)
 		// user TOTP APIs
 		// user TOTP APIs
 		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Get(userTOTPConfigsPath, getTOTPConfigs)
 			Get(userTOTPConfigsPath, getTOTPConfigs)
@@ -1092,7 +1097,8 @@ func (s *httpdServer) initializeRouter() {
 			router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
 			router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
 			router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
 			router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
 				Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
 				Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
-			router.Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost)
+			router.With(checkHTTPUserPerm(sdk.WebClientAPIKeyAuthChangeDisabled)).
+				Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost)
 			router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
 			router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
 				Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
 				Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
 			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
 			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).

+ 6 - 5
sdk/user.go

@@ -9,16 +9,17 @@ import (
 
 
 // Web Client/user REST API restrictions
 // Web Client/user REST API restrictions
 const (
 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 (
 var (
 	// WebClientOptions defines the available options for the web client interface/user REST API
 	// WebClientOptions defines the available options for the web client interface/user REST API
 	WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled, WebClientMFADisabled,
 	WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled, WebClientMFADisabled,
-		WebClientPasswordChangeDisabled}
+		WebClientPasswordChangeDisabled, WebClientAPIKeyAuthChangeDisabled}
 	// UserTypes defines the supported user type hints for auth plugins
 	// UserTypes defines the supported user type hints for auth plugins
 	UserTypes = []string{string(UserTypeLDAP), string(UserTypeOS)}
 	UserTypes = []string{string(UserTypeLDAP), string(UserTypeOS)}
 )
 )

+ 3 - 2
templates/webclient/credentials.html

@@ -110,7 +110,7 @@
         <form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
         <form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
             <div class="form-group">
             <div class="form-group">
                 <div class="form-check">
                 <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">
                     {{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
                     <label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
                     <label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
                     <small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
                     <small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
@@ -118,9 +118,10 @@
                     </small>
                     </small>
                 </div>
                 </div>
             </div>
             </div>
-
+            {{if .LoggedUser.CanChangeAPIKeyAuth}}
             <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
             <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>
             <button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
+            {{end}}
         </form>
         </form>
     </div>
     </div>
 </div>
 </div>