diff --git a/pkg/api/admin/administration.go b/pkg/api/admin/administration.go index 9f1a361..b14585d 100644 --- a/pkg/api/admin/administration.go +++ b/pkg/api/admin/administration.go @@ -667,3 +667,30 @@ func GetGroupConfig(w http.ResponseWriter, r *http.Request) { } logger.WithResponseStatus(customLogger, w).Info("get group config") } + +func RevokeNode(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + group := vars["groupname"] + nodeId := vars["nodeId"] + + customlogger := logger.NewLoggerFromContext(r.Context()).WithFields(log.Fields{"groupname": group, "nodeId": nodeId}) + + vaultClient, rksErr := vault.NewVaultClientFromHTTPRequest(r) + if rksErr != nil { + rksErr.HandleErr(r.Context(), w) + return + } + if exists, rksErr := vaultClient.GroupExists(group); rksErr != nil { + rksErr.HandleErr(r.Context(), w) + return + } else if !exists { + (&model.RksError{WrappedError: nil, Message: "Group not found", Code: 404}).HandleErr(r.Context(), w) + return + } + if rksErr = vaultClient.RevokeNodeToken(group, nodeId); rksErr != nil { + rksErr.HandleErr(r.Context(), w) + return + } + w.WriteHeader(http.StatusNoContent) + customlogger.Info("revoke node") +} diff --git a/pkg/api/routers.go b/pkg/api/routers.go index 8e42cc9..fe6628b 100644 --- a/pkg/api/routers.go +++ b/pkg/api/routers.go @@ -245,4 +245,10 @@ var routes = map[string]Route{"Index": {"GET", "/", false, Index}, "Login": { false, node.RegisterNode, }, + "RevokeNode": { + strings.ToUpper("Delete"), + "/rks/v1/group/{groupname:[a-zA-Z0-9\\-]{1,64}}/nodes/{nodeId}", + false, + admin.RevokeNode, + }, } diff --git a/pkg/vault/client.go b/pkg/vault/client.go index 51d15a0..b00d20d 100644 --- a/pkg/vault/client.go +++ b/pkg/vault/client.go @@ -444,6 +444,16 @@ func (v *Vault) CreateNodeTokenRole(group string, nodeID string) *model.RksError return nil } +func (v *Vault) RevokeNodeToken(group string, nodeID string) *model.RksError { + url := fmt.Sprintf("/auth/token/create/%s-%s", group, nodeID) + + err := v.Sys().RevokePrefix(url) + if err != nil { + return RKSErrFromVaultErr(err, fmt.Sprintf("Couldn't Revoke Token for node %s in groupname %s unable to register node ", nodeID, group)) + } + return nil +} + func (v *Vault) CreateNodeTokenFromRole(group string, nodeID string) (*vaultAPI.Secret, *model.RksError) { url := fmt.Sprintf("/auth/token/create/%s-%s", group, nodeID) auth, err := v.Logical().Write(url, map[string]interface{}{}) diff --git a/pkg/vault/policy.go b/pkg/vault/policy.go index da05fa6..2ea538d 100644 --- a/pkg/vault/policy.go +++ b/pkg/vault/policy.go @@ -58,6 +58,10 @@ path "sys/policies/*" { capabilities = ["create","update","read","delete"] } +path "sys/leases/revoke-prefix/auth/token/*" { + capabilities = ["sudo","create","update","read","delete"] +} + path "auth/token/lookup-self" { capabilities = ["read"] } diff --git a/rks-openapi.yaml b/rks-openapi.yaml index 4135985..a2964f6 100644 --- a/rks-openapi.yaml +++ b/rks-openapi.yaml @@ -220,6 +220,29 @@ paths: description: "Forbidden. Invalid X-Vault-Token" '404': description: Group not found + '/rks/v1/group/{groupname}/nodes/{nodeId}': + delete: + summary: Revoke a Node by revoking its token + description: | + revoke the **nodeToken** for a node part of group **groupname** with nodeId **X-LCDN-nodeId**, + **adminToken** is used as X-Vault-Token for authentication. + Note that this call will revoke the **nodeToken** but won't forbid the node to register again. + To forbid node registration there are two ways: + Make group manager callback url answer "40X" for this nodeId + Renew the group grouptoken and spread it to all nodes, except this one + tags: + - RKS Administration + operationId: revokeNode + parameters: + - $ref: '#/components/parameters/groupname' + - $ref: '#/components/parameters/nodeIdPath' + responses: + '204': + description: Node successfully deleted + '403': + description: "Forbidden. Invalid X-Vault-Token" + '404': + description: Group not found '/rks/v1/group/{groupname}/secrets': get: summary: Get secrets associated with **groupname** @@ -297,7 +320,7 @@ paths: - RKS Node Setup operationId: registerNode parameters: - - $ref: '#/components/parameters/nodeId' + - $ref: '#/components/parameters/nodeIdHeader' responses: '201': description: Node successfully registered @@ -460,7 +483,17 @@ components: schema: type: string example: "test.com" - nodeId: + nodeIdPath: + name: nodeId + in: path + required: true + description: | + **nodeID** + schema: + type: string + example: "8c03fb4a-007f-11ea-9383-0fd702e68c30" + pattern: "^[a-zA-Z0-9\\-]{1,64}$" + nodeIdHeader: name: X-LCDN-nodeId in: header required: true @@ -608,4 +641,3 @@ components: +9xiOEcTNrlo9vuonkMoyiwDua6H90vv3sgBJ80WDTn6sW/SXRTwDbE8Dn9ODv65 adVJ9a+aAf+SYT8HpMNezvro -----END PRIVATE KEY----- - diff --git a/tests/test/test_rks_administration_api.py b/tests/test/test_rks_administration_api.py index c12d405..bd97023 100644 --- a/tests/test/test_rks_administration_api.py +++ b/tests/test/test_rks_administration_api.py @@ -435,3 +435,45 @@ def test_get_secrets_groups(self, admin_api, group_token, test_dot_com_secret): with pytest.raises(ApiException) as excinfo: group_names = admin_api.get_secret_groups("testnotexist.com") assert excinfo.value.status == 404 + + def test_revoke_node( + self, + admin_api, + secret_api, + node_token, + associate_dot_com_group, + test_dot_com_secret, + ): + + secret_api.api_client.configuration.api_key["X-Vault-Token"] = node_token + + secret = secret_api.get_secret("test.com") + + assert secret.data != None, "revoke failed" + assert secret.data.certificate != None, "revoke failed" + assert secret.data.private_key != None, "revoke failed" + assert secret.data.meta != None, "revoke failed" + + _, status, headers = admin_api.revoke_node_with_http_info("fakecdn1", "1") + + assert status == 204, "revoke node does not return 204 as expected" + + try: + secret = secret_api.get_secret("test.com") + except ApiException as e: + assert ( + e.status == 403 + ), "getting secret with revoked nodeToken does not return 403 as expected" + + # test revoke unknown nodeId + _, status, headers = admin_api.revoke_node_with_http_info("fakecdn1", "10") + + assert status == 204, "revoke unknown nodeId does not return 204 as expected" + + with pytest.raises(ApiException) as ex: + # test revoke unknown groupname nodeId + admin_api.revoke_node("unknowngroupname", "1") + + assert ( + ex.value.status == 404 + ), "revoke nodeId from unknown group does not return 404 as expected"