diff --git a/dataprovider/user.go b/dataprovider/user.go index 6087bf2a..53bea869 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -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 { diff --git a/httpd/api_admin.go b/httpd/api_admin.go index 1d6c451a..a99c2a1c 100644 --- a/httpd/api_admin.go +++ b/httpd/api_admin.go @@ -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) diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index 136ec139..0f465d81 100644 --- a/httpd/api_http_user.go +++ b/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) } +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) diff --git a/httpd/api_utils.go b/httpd/api_utils.go index db285494..c5edfead 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -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 { diff --git a/httpd/httpd.go b/httpd/httpd.go index e14bc577..cd5a3b27 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -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" diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index f80d6c95..f3ee2d15 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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) diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 7cbdc8c7..c32df4a8 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -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() diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index a3d211a0..020aa3dd 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -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: diff --git a/httpd/server.go b/httpd/server.go index fbd2a063..6cce30e8 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -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). diff --git a/sdk/user.go b/sdk/user.go index 04781a52..168fca64 100644 --- a/sdk/user.go +++ b/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)} ) diff --git a/templates/webclient/credentials.html b/templates/webclient/credentials.html index 287696ad..1a97b8f6 100644 --- a/templates/webclient/credentials.html +++ b/templates/webclient/credentials.html @@ -110,7 +110,7 @@