diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml index d475d4cc6..5a886c1ee 100644 --- a/.github/workflows/apiserver-test.yaml +++ b/.github/workflows/apiserver-test.yaml @@ -92,10 +92,10 @@ jobs: kubectl wait --for=condition=Ready pod -l app=source-controller -n flux-system --timeout=600s kubectl wait --for=condition=Ready pod -l app=helm-controller -n flux-system --timeout=600s - - name: Run apiserver unit test + - name: Run api server unit test run: make unit-test-apiserver - - name: Run apiserver e2e test + - name: Run api server e2e test run: | export ALIYUN_ACCESS_KEY_ID=${{ secrets.ALIYUN_ACCESS_KEY_ID }} export ALIYUN_ACCESS_KEY_SECRET=${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 4f5f08a99..ed1e2ef31 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -120,5 +120,5 @@ func (s *Server) buildSwagger() (*spec.Swagger, error) { if err != nil { return nil, fmt.Errorf("create apiserver failed : %w ", err) } - return restfulspec.BuildSwagger(server.RegisterServices()), nil + return restfulspec.BuildSwagger(server.RegisterServices(context.Background(), false)), nil } diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index f7db70bf8..e69d70a49 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -85,7 +85,7 @@ } } }, - "/api/v1/addon_registries/{name}": { + "/api/v1/addon_registries/{addonRegName}": { "put": { "consumes": [ "application/xml", @@ -112,7 +112,7 @@ { "type": "string", "description": "identifier of the addon registry", - "name": "name", + "name": "addonRegName", "in": "path", "required": true } @@ -150,7 +150,7 @@ { "type": "string", "description": "identifier of the addon registry", - "name": "name", + "name": "addonRegName", "in": "path", "required": true } @@ -216,7 +216,7 @@ } } }, - "/api/v1/addons/{name}": { + "/api/v1/addons/{addonName}": { "get": { "consumes": [ "application/xml", @@ -235,7 +235,7 @@ { "type": "string", "description": "addon name to query detail", - "name": "name", + "name": "addonName", "in": "path", "required": true }, @@ -262,7 +262,7 @@ } } }, - "/api/v1/addons/{name}/disable": { + "/api/v1/addons/{addonName}/disable": { "post": { "consumes": [ "application/xml", @@ -281,7 +281,7 @@ { "type": "string", "description": "addon name to enable", - "name": "name", + "name": "addonName", "in": "path", "required": true }, @@ -308,7 +308,7 @@ } } }, - "/api/v1/addons/{name}/enable": { + "/api/v1/addons/{addonName}/enable": { "post": { "consumes": [ "application/xml", @@ -335,7 +335,7 @@ { "type": "string", "description": "addon name to enable", - "name": "name", + "name": "addonName", "in": "path", "required": true } @@ -356,7 +356,7 @@ } } }, - "/api/v1/addons/{name}/status": { + "/api/v1/addons/{addonName}/status": { "get": { "consumes": [ "application/xml", @@ -375,7 +375,7 @@ { "type": "string", "description": "addon name to query status", - "name": "name", + "name": "addonName", "in": "path", "required": true } @@ -396,7 +396,7 @@ } } }, - "/api/v1/addons/{name}/update": { + "/api/v1/addons/{addonName}/update": { "put": { "consumes": [ "application/xml", @@ -423,7 +423,7 @@ { "type": "string", "description": "addon name to update", - "name": "name", + "name": "addonName", "in": "path", "required": true } @@ -468,8 +468,14 @@ }, { "type": "string", - "description": "The namespace of the managed cluster", - "name": "namespace", + "description": "search base on project name", + "name": "project", + "in": "query" + }, + { + "type": "string", + "description": "search base on env name", + "name": "env", "in": "query" }, { @@ -534,7 +540,7 @@ } } }, - "/api/v1/applications/{name}": { + "/api/v1/applications/{appName}": { "get": { "consumes": [ "application/xml", @@ -553,7 +559,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -591,7 +597,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -637,7 +643,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -658,7 +664,7 @@ } } }, - "/api/v1/applications/{name}/compare": { + "/api/v1/applications/{appName}/compare": { "post": { "consumes": [ "application/xml", @@ -677,7 +683,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -698,7 +704,7 @@ } } }, - "/api/v1/applications/{name}/components": { + "/api/v1/applications/{appName}/components": { "get": { "consumes": [ "application/xml", @@ -717,7 +723,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -761,7 +767,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -790,7 +796,7 @@ } } }, - "/api/v1/applications/{name}/components/{compName}": { + "/api/v1/applications/{appName}/components/{compName}": { "get": { "consumes": [ "application/xml", @@ -809,7 +815,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -854,7 +860,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -907,7 +913,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -941,7 +947,7 @@ } } }, - "/api/v1/applications/{name}/components/{compName}/traits": { + "/api/v1/applications/{appName}/components/{compName}/traits": { "post": { "consumes": [ "application/xml", @@ -960,7 +966,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -996,7 +1002,7 @@ } } }, - "/api/v1/applications/{name}/components/{compName}/traits/{traitType}": { + "/api/v1/applications/{appName}/components/{compName}/traits/{traitType}": { "put": { "consumes": [ "application/xml", @@ -1015,7 +1021,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1075,7 +1081,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1110,7 +1116,7 @@ } } }, - "/api/v1/applications/{name}/deploy": { + "/api/v1/applications/{appName}/deploy": { "post": { "consumes": [ "application/xml", @@ -1129,7 +1135,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1158,7 +1164,7 @@ } } }, - "/api/v1/applications/{name}/dry-run": { + "/api/v1/applications/{appName}/dry-run": { "post": { "consumes": [ "application/xml", @@ -1177,7 +1183,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -1198,7 +1204,7 @@ } } }, - "/api/v1/applications/{name}/envs": { + "/api/v1/applications/{appName}/envs": { "get": { "consumes": [ "application/xml", @@ -1217,7 +1223,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -1255,7 +1261,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1284,7 +1290,7 @@ } } }, - "/api/v1/applications/{name}/envs/{envName}": { + "/api/v1/applications/{appName}/envs/{envName}": { "put": { "consumes": [ "application/xml", @@ -1303,7 +1309,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1356,7 +1362,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1384,7 +1390,7 @@ } } }, - "/api/v1/applications/{name}/envs/{envName}/recycle": { + "/api/v1/applications/{appName}/envs/{envName}/recycle": { "post": { "consumes": [ "application/xml", @@ -1403,7 +1409,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1431,7 +1437,7 @@ } } }, - "/api/v1/applications/{name}/envs/{envName}/status": { + "/api/v1/applications/{appName}/envs/{envName}/status": { "get": { "consumes": [ "application/xml", @@ -1450,7 +1456,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1478,7 +1484,7 @@ } } }, - "/api/v1/applications/{name}/policies": { + "/api/v1/applications/{appName}/policies": { "get": { "consumes": [ "application/xml", @@ -1497,7 +1503,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -1535,7 +1541,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1564,7 +1570,7 @@ } } }, - "/api/v1/applications/{name}/policies/{policyName}": { + "/api/v1/applications/{appName}/policies/{policyName}": { "get": { "consumes": [ "application/xml", @@ -1583,7 +1589,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1628,7 +1634,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1681,7 +1687,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1709,7 +1715,7 @@ } } }, - "/api/v1/applications/{name}/records": { + "/api/v1/applications/{appName}/records": { "get": { "consumes": [ "application/xml", @@ -1728,7 +1734,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -1746,7 +1752,7 @@ } } }, - "/api/v1/applications/{name}/reset": { + "/api/v1/applications/{appName}/reset": { "post": { "consumes": [ "application/xml", @@ -1765,7 +1771,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -1786,7 +1792,7 @@ } } }, - "/api/v1/applications/{name}/revisions": { + "/api/v1/applications/{appName}/revisions": { "get": { "consumes": [ "application/xml", @@ -1805,7 +1811,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1850,7 +1856,7 @@ } } }, - "/api/v1/applications/{name}/revisions/{revision}": { + "/api/v1/applications/{appName}/revisions/{revision}": { "get": { "consumes": [ "application/xml", @@ -1869,7 +1875,7 @@ { "type": "string", "description": "identifier of the application", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1897,7 +1903,7 @@ } } }, - "/api/v1/applications/{name}/statistics": { + "/api/v1/applications/{appName}/statistics": { "get": { "consumes": [ "application/xml", @@ -1916,7 +1922,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -1937,7 +1943,7 @@ } } }, - "/api/v1/applications/{name}/template": { + "/api/v1/applications/{appName}/template": { "post": { "consumes": [ "application/xml", @@ -1956,7 +1962,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -1985,7 +1991,7 @@ } } }, - "/api/v1/applications/{name}/triggers": { + "/api/v1/applications/{appName}/triggers": { "get": { "consumes": [ "application/xml", @@ -2004,7 +2010,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -2042,7 +2048,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2071,7 +2077,7 @@ } } }, - "/api/v1/applications/{name}/triggers/{token}": { + "/api/v1/applications/{appName}/triggers/{token}": { "delete": { "consumes": [ "application/xml", @@ -2090,7 +2096,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2118,7 +2124,7 @@ } } }, - "/api/v1/applications/{name}/workflows": { + "/api/v1/applications/{appName}/workflows": { "get": { "consumes": [ "application/xml", @@ -2137,7 +2143,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -2180,7 +2186,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true } @@ -2204,7 +2210,7 @@ } } }, - "/api/v1/applications/{name}/workflows/{workflowName}": { + "/api/v1/applications/{appName}/workflows/{workflowName}": { "get": { "consumes": [ "application/xml", @@ -2223,7 +2229,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2265,7 +2271,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2315,7 +2321,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2340,7 +2346,7 @@ } } }, - "/api/v1/applications/{name}/workflows/{workflowName}/records": { + "/api/v1/applications/{appName}/workflows/{workflowName}/records": { "get": { "consumes": [ "application/xml", @@ -2359,7 +2365,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2396,7 +2402,7 @@ } } }, - "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}": { + "/api/v1/applications/{appName}/workflows/{workflowName}/records/{record}": { "get": { "consumes": [ "application/xml", @@ -2415,7 +2421,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2447,7 +2453,7 @@ } } }, - "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/resume": { + "/api/v1/applications/{appName}/workflows/{workflowName}/records/{record}/resume": { "get": { "consumes": [ "application/xml", @@ -2466,7 +2472,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2498,7 +2504,7 @@ } } }, - "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/rollback": { + "/api/v1/applications/{appName}/workflows/{workflowName}/records/{record}/rollback": { "get": { "consumes": [ "application/xml", @@ -2517,7 +2523,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2555,7 +2561,7 @@ } } }, - "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/terminate": { + "/api/v1/applications/{appName}/workflows/{workflowName}/records/{record}/terminate": { "get": { "consumes": [ "application/xml", @@ -2574,7 +2580,7 @@ { "type": "string", "description": "identifier of the application.", - "name": "name", + "name": "appName", "in": "path", "required": true }, @@ -2606,7 +2612,7 @@ } } }, - "/api/v1/auth/dexConfig": { + "/api/v1/auth/dex_config": { "get": { "consumes": [ "application/xml", @@ -2674,7 +2680,7 @@ } } }, - "/api/v1/auth/loginType": { + "/api/v1/auth/login_type": { "get": { "consumes": [ "application/xml", @@ -2703,7 +2709,7 @@ } } }, - "/api/v1/auth/refreshToken": { + "/api/v1/auth/refresh_token": { "get": { "consumes": [ "application/xml", @@ -2732,6 +2738,35 @@ } } }, + "/api/v1/auth/user_info": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "authentication" + ], + "summary": "get login user detail info", + "operationId": "getLoginUserInfo", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.LoginUserInfoResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/clusters": { "get": { "consumes": [ @@ -3343,7 +3378,7 @@ } } }, - "/api/v1/definitions/{name}": { + "/api/v1/definitions/{definitionName}": { "get": { "consumes": [ "application/xml", @@ -3362,7 +3397,7 @@ { "type": "string", "description": "identifier of the definition", - "name": "name", + "name": "definitionName", "in": "path", "required": true }, @@ -3489,7 +3524,7 @@ } } }, - "/api/v1/envs/{name}": { + "/api/v1/envs/{envName}": { "put": { "consumes": [ "application/xml", @@ -3508,7 +3543,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "envName", "in": "path", "required": true }, @@ -3548,7 +3583,7 @@ { "type": "string", "description": "identifier of the application ", - "name": "name", + "name": "envName", "in": "path", "required": true } @@ -3597,6 +3632,34 @@ } } }, + "/api/v1/permissions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "rbac" + ], + "summary": "list all project level perm policies", + "operationId": "listPlatformPermissions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PermissionBase" + } + } + } + } + } + }, "/api/v1/policy_definitions": { "get": { "consumes": [ @@ -3680,6 +3743,515 @@ } } }, + "/api/v1/projects/{projectName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "detail a project", + "operationId": "detailProject", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ProjectBase" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "update a project", + "operationId": "updateProject", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateProjectRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ProjectBase" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "delete a project", + "operationId": "deleteProject", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + } + } + } + }, + "/api/v1/projects/{projectName}/permissions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "list all project level perm policies", + "operationId": "listProjectPermissions", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PermissionBase" + } + } + } + } + } + }, + "/api/v1/projects/{projectName}/roles": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "list all project level roles", + "operationId": "listProjectRoles", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListRolesResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "create project level role", + "operationId": "createProjectRole", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.RoleBase" + } + } + } + } + }, + "/api/v1/projects/{projectName}/roles/{roleName}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "update project level role", + "operationId": "updateProjectRole", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the project role", + "name": "roleName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.RoleBase" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "delete project level role", + "operationId": "deleteProjectRole", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the project role", + "name": "roleName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + } + } + } + }, + "/api/v1/projects/{projectName}/targets": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "get targets list belong to a project", + "operationId": "listProjectTargets", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + } + } + } + }, + "/api/v1/projects/{projectName}/users": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "list all users belong to a project", + "operationId": "listProjectUser", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListProjectUsersResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "add a user to a project", + "operationId": "createProjectUser", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.AddProjectUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ProjectUserBase" + } + } + } + } + }, + "/api/v1/projects/{projectName}/users/{userName}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "add a user to a project", + "operationId": "updateProjectUser", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateProjectUserRequest" + } + }, + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the project user", + "name": "userName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ProjectUserBase" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "delete a user from a project", + "operationId": "deleteProjectUser", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateProjectUserRequest" + } + }, + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the project user", + "name": "userName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + } + } + } + }, "/api/v1/query": { "get": { "consumes": [ @@ -3842,6 +4414,122 @@ } } }, + "/api/v1/roles": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "rbac" + ], + "summary": "list all platform level roles", + "operationId": "listPlatformRoles", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListRolesResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "rbac" + ], + "summary": "create platform level role", + "operationId": "createPlatformRole", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.RoleBase" + } + } + } + } + }, + "/api/v1/roles/{roleName}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "rbac" + ], + "summary": "update platform level role", + "operationId": "updatePlatformRole", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.RoleBase" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "rbac" + ], + "summary": "update platform level role", + "operationId": "deletePlatformRole", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + } + } + } + }, "/api/v1/system_info": { "get": { "consumes": [ @@ -3908,34 +4596,6 @@ } } } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "systemInfo" - ], - "operationId": "deleteSystemInfo", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.SystemInfoResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } } }, "/api/v1/targets": { @@ -3965,6 +4625,12 @@ "description": "PageSize for paging", "name": "pageSize", "in": "query" + }, + { + "type": "string", + "description": "list targets by project name", + "name": "project", + "in": "query" } ], "responses": { @@ -4022,7 +4688,7 @@ } } }, - "/api/v1/targets/{name}": { + "/api/v1/targets/{targetName}": { "get": { "consumes": [ "application/xml", @@ -4041,7 +4707,7 @@ { "type": "string", "description": "identifier of the Target.", - "name": "name", + "name": "targetName", "in": "path", "required": true } @@ -4076,7 +4742,7 @@ { "type": "string", "description": "identifier of the Target", - "name": "name", + "name": "targetName", "in": "path", "required": true }, @@ -4119,7 +4785,7 @@ { "type": "string", "description": "identifier of the Target", - "name": "name", + "name": "targetName", "in": "path", "required": true } @@ -5581,8 +6247,8 @@ }, "model.ApplicationRevision": { "required": [ - "updateTime", "createTime", + "updateTime", "appPrimaryKey", "version", "status", @@ -5878,8 +6544,8 @@ }, "model.SystemInfo": { "required": [ - "updateTime", "createTime", + "updateTime", "installID", "enableCollection", "loginType" @@ -6223,6 +6889,23 @@ } } }, + "v1.AddProjectUserRequest": { + "required": [ + "userName", + "userRoles" + ], + "properties": { + "userName": { + "type": "string" + }, + "userRoles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.AddonBaseStatus": { "required": [ "name", @@ -6433,12 +7116,12 @@ }, "v1.ApplicationDeployResponse": { "required": [ + "version", "status", - "note", + "envName", "triggerType", "createTime", - "version", - "envName" + "note" ], "properties": { "codeInfo": { @@ -7379,12 +8062,37 @@ }, "name": { "type": "string" + }, + "owner": { + "type": "string" + } + } + }, + "v1.CreateRoleRequest": { + "required": [ + "name", + "alias", + "permissions" + ], + "properties": { + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } } } }, "v1.CreateTargetRequest": { "required": [ - "name" + "name", + "project" ], "properties": { "alias": { @@ -7399,6 +8107,9 @@ "name": { "type": "string" }, + "project": { + "type": "string" + }, "variable": { "type": "object" } @@ -7408,7 +8119,8 @@ "required": [ "name", "email", - "password" + "password", + "roles" ], "properties": { "alias": { @@ -7422,6 +8134,12 @@ }, "password": { "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -7490,11 +8208,11 @@ }, "v1.DetailAddonResponse": { "required": [ - "name", "version", "description", "icon", "invisible", + "name", "schema", "uiSchema", "definitions" @@ -7567,13 +8285,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "description", + "name", "alias", "project", "createTime", "updateTime", + "description", "icon", - "name", "policies", "envBindings", "applicationType", @@ -7634,20 +8352,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "reason", - "labels", - "updateTime", - "alias", - "icon", - "provider", + "status", "kubeConfig", "createTime", - "description", + "labels", + "reason", + "provider", + "kubeConfigSecret", + "icon", "name", + "alias", + "description", "apiServerURL", "dashboardURL", - "kubeConfigSecret", - "status", + "updateTime", "resourceInfo" ], "properties": { @@ -7705,14 +8423,14 @@ }, "v1.DetailComponentResponse": { "required": [ - "alias", - "creator", - "main", "appPrimaryKey", + "main", + "creator", "name", "type", "createTime", "updateTime", + "alias", "definition" ], "properties": { @@ -7814,13 +8532,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "description", + "creator", + "properties", "createTime", "updateTime", "name", - "type", - "description", - "creator", - "properties" + "type" ], "properties": { "createTime": { @@ -7850,17 +8568,17 @@ }, "v1.DetailRevisionResponse": { "required": [ + "reason", + "deployUser", + "workflowName", + "createTime", "updateTime", - "note", "appPrimaryKey", "version", - "createTime", - "reason", - "triggerType", - "envName", "status", - "deployUser", - "workflowName" + "note", + "triggerType", + "envName" ], "properties": { "appPrimaryKey": { @@ -7916,6 +8634,7 @@ "required": [ "createTime", "updateTime", + "project", "name" ], "properties": { @@ -7942,6 +8661,9 @@ "name": { "type": "string" }, + "project": { + "$ref": "#/definitions/v1.NameAlias" + }, "updateTime": { "type": "string", "format": "date-time" @@ -7958,7 +8680,8 @@ "name", "email", "disabled", - "projects" + "projects", + "roles" ], "properties": { "alias": { @@ -7984,7 +8707,13 @@ "projects": { "type": "array", "items": { - "$ref": "#/definitions/v1.ProjectUserBase" + "$ref": "#/definitions/v1.ProjectBase" + } + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NameAlias" } } } @@ -8049,13 +8778,13 @@ "v1.DetailWorkflowResponse": { "required": [ "name", + "enable", + "default", + "createTime", + "updateTime", "alias", "description", - "enable", - "envName", - "createTime", - "default", - "updateTime" + "envName" ], "properties": { "alias": { @@ -8477,6 +9206,24 @@ } } }, + "v1.ListProjectUsersResponse": { + "required": [ + "users", + "total" + ], + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ProjectUserBase" + } + } + } + }, "v1.ListRevisionsResponse": { "required": [ "revisions", @@ -8495,6 +9242,24 @@ } } }, + "v1.ListRolesResponse": { + "required": [ + "total", + "roles" + ], + "properties": { + "roles": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.RoleBase" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, "v1.ListTargetResponse": { "required": [ "targets", @@ -8593,6 +9358,61 @@ } } }, + "v1.LoginUserInfoResponse": { + "required": [ + "disabled", + "createTime", + "lastLoginTime", + "name", + "email", + "projects", + "platformPermissions", + "projectPermissions" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "disabled": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "lastLoginTime": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "platformPermissions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PermissionBase" + } + }, + "projectPermissions": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PermissionBase" + } + } + }, + "projects": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ProjectBase" + } + } + } + }, "v1.NameAlias": { "required": [ "name", @@ -8640,6 +9460,48 @@ } } }, + "v1.PermissionBase": { + "required": [ + "name", + "alias", + "resources", + "actions", + "effect", + "createTime", + "updateTime" + ], + "properties": { + "actions": { + "type": "array", + "items": { + "type": "string" + } + }, + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "effect": { + "type": "string" + }, + "name": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "v1.PolicyBase": { "required": [ "name", @@ -8719,6 +9581,9 @@ "name": { "type": "string" }, + "owner": { + "$ref": "#/definitions/v1.NameAlias" + }, "updateTime": { "type": "string", "format": "date-time" @@ -8728,16 +9593,22 @@ "v1.ProjectUserBase": { "required": [ "name", - "alias", - "userRoles" + "userRoles", + "createTime", + "updateTime" ], "properties": { - "alias": { - "type": "string" + "createTime": { + "type": "string", + "format": "date-time" }, "name": { "type": "string" }, + "updateTime": { + "type": "string", + "format": "date-time" + }, "userRoles": { "type": "array", "items": { @@ -8761,6 +9632,36 @@ } } }, + "v1.RoleBase": { + "required": [ + "createTime", + "updateTime", + "name", + "permissions" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NameAlias" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "v1.SimpleResponse": { "required": [ "status" @@ -8787,10 +9688,10 @@ }, "v1.SystemInfoResponse": { "required": [ + "updateTime", "installID", "enableCollection", "loginType", - "updateTime", "createTime", "systemVersion" ], @@ -8835,7 +9736,8 @@ "required": [ "name", "createTime", - "updateTime" + "updateTime", + "project" ], "properties": { "alias": { @@ -8861,6 +9763,9 @@ "name": { "type": "string" }, + "project": { + "$ref": "#/definitions/v1.NameAlias" + }, "updateTime": { "type": "string", "format": "date-time" @@ -8967,6 +9872,49 @@ } } }, + "v1.UpdateProjectRequest": { + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "owner": { + "type": "string" + } + } + }, + "v1.UpdateProjectUserRequest": { + "required": [ + "userRoles" + ], + "properties": { + "userRoles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.UpdateRoleRequest": { + "required": [ + "alias", + "permissions" + ], + "properties": { + "alias": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.UpdateTargetRequest": { "properties": { "alias": { diff --git a/docs/examples/rbac/rbac.md b/docs/examples/rbac/rbac.md new file mode 100644 index 000000000..bac721e19 --- /dev/null +++ b/docs/examples/rbac/rbac.md @@ -0,0 +1,107 @@ +# RBAC + +User: + +```yaml +name: user +userRoles: ["app-developer"] +... +``` + +ProjectUser: + +```yaml +username: user +project: demo +userRoles: ["app-developer"] +``` + +Role: + +```yaml +name: app-developer +project: demo +permissions: ["app-manage"] +``` + +```yaml +name: admin +permissions: ["all"] +``` + +Permission: + +```yaml +name: app-manage +project: demo +resource: ["project:demo/application:*"] +actions: ["*"] +effect: Allow +principal: {} +condition: {} +``` + +```yaml +name: app1-manage +project: demo +resource: ["project:demo/application:app1/*"] +actions: ["*"] +effect: Allow +principal: {} +condition: {} + +name: app2-manage +project: demo +resource: ["project:demo/application:app2/*"] +actions: ["*"] +effect: Allow +principal: {} +condition: {} +``` + +```yaml +name: cluster-manage +resource: ["cluster:*"] +actions: ["*"] +effect: Allow +principal: {} +condition: {} +``` + +```yaml +name: cluster-beijing-manage +resource: ["cluster:beijing"] +actions: ["*"] +effect: Allow +principal: {} +condition: {} +``` + +```yaml +name: all +resource: ["*"] +actions: ["*"] +effect: Allow +principal: {} +condition: {} +``` + +PermissionTemplate: + +```yaml +name: app-manage +resource: ["project:${projectName}/application:*"] +actions: ["*"] +level: project +effect: Allow +principal: {} +condition: {} +``` + +```yaml +name: deny-delete-cluster +resource: ["cluster:*"] +actions: ["delete"] +level: platform +effect: Deny +``` diff --git a/pkg/apiserver/datastore/datastore.go b/pkg/apiserver/datastore/datastore.go index 320aeb7d0..aa5cd09f1 100644 --- a/pkg/apiserver/datastore/datastore.go +++ b/pkg/apiserver/datastore/datastore.go @@ -112,9 +112,22 @@ type FuzzyQueryOption struct { Query string } +// InQueryOption defines the include search filter option +type InQueryOption struct { + Key string + Values []string +} + +// IsNotExistQueryOption means the value is empty +type IsNotExistQueryOption struct { + Key string +} + // FilterOptions filter query returned items type FilterOptions struct { - Queries []FuzzyQueryOption + Queries []FuzzyQueryOption + In []InQueryOption + IsNotExist []IsNotExistQueryOption } // ListOptions list api options diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index bd0a5bc5a..b5687e2bb 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -120,23 +120,23 @@ func (m *kubeapi) Add(ctx context.Context, entity datastore.Entity) error { } // BatchAdd batch add entity, this operation has some atomicity. -func (m *kubeapi) BatchAdd(ctx context.Context, entitys []datastore.Entity) error { - donotRollback := make(map[string]int) - for i, saveEntity := range entitys { +func (m *kubeapi) BatchAdd(ctx context.Context, entities []datastore.Entity) error { + notRollback := make(map[string]int) + for i, saveEntity := range entities { if err := m.Add(ctx, saveEntity); err != nil { if errors.Is(err, datastore.ErrRecordExist) { - donotRollback[saveEntity.PrimaryKey()] = 1 + notRollback[saveEntity.PrimaryKey()] = 1 } - for _, deleteEntity := range entitys[:i] { - if _, exit := donotRollback[deleteEntity.PrimaryKey()]; !exit { + for _, deleteEntity := range entities[:i] { + if _, exit := notRollback[deleteEntity.PrimaryKey()]; !exit { if err := m.Delete(ctx, deleteEntity); err != nil { if !errors.Is(err, datastore.ErrRecordNotExist) { - log.Logger.Errorf("rollback delete component failure %w", err) + log.Logger.Errorf("rollback delete entity failure %w", err) } } } } - return datastore.NewDBError(fmt.Errorf("save components occur error, %w", err)) + return datastore.NewDBError(fmt.Errorf("save entities occur error, %w", err)) } } return nil @@ -347,6 +347,24 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } selector = selector.Add(*rq) } + if op != nil { + for _, inFilter := range op.In { + rq, err := labels.NewRequirement(inFilter.Key, selection.In, inFilter.Values) + if err != nil { + log.Logger.Errorf("new list requirement failure %s", err.Error()) + return nil, datastore.ErrIndexInvalid + } + selector = selector.Add(*rq) + } + for _, notFilter := range op.IsNotExist { + rq, err := labels.NewRequirement(notFilter.Key, selection.DoesNotExist, []string{}) + if err != nil { + log.Logger.Errorf("new list requirement failure %s", err.Error()) + return nil, datastore.ErrIndexInvalid + } + selector = selector.Add(*rq) + } + } options := &client.ListOptions{ LabelSelector: selector, Namespace: m.namespace, @@ -385,7 +403,6 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto items = items[:limit] } var list []datastore.Entity - log.Logger.Debugf("query %s result count %d", selector, len(items)) for _, item := range items { ent, err := datastore.NewEntity(entity) if err != nil { @@ -416,6 +433,24 @@ func (m *kubeapi) Count(ctx context.Context, entity datastore.Entity, filterOpti } selector = selector.Add(*rq) } + if filterOptions != nil { + for _, inFilter := range filterOptions.In { + rq, err := labels.NewRequirement(inFilter.Key, selection.In, inFilter.Values) + if err != nil { + return 0, datastore.ErrIndexInvalid + } + selector = selector.Add(*rq) + } + for _, notFilter := range filterOptions.IsNotExist { + rq, err := labels.NewRequirement(notFilter.Key, selection.DoesNotExist, []string{}) + if err != nil { + log.Logger.Errorf("new list requirement failure %s", err.Error()) + return 0, datastore.ErrIndexInvalid + } + selector = selector.Add(*rq) + } + } + options := &client.ListOptions{ LabelSelector: selector, Namespace: m.namespace, diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index aab4c41c3..07c82ee27 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -96,7 +96,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, &model.Application{Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Name: "kubevela-app-4", Project: "testProject", Description: "this is demo 4"}, } err := kubeStore.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) @@ -106,7 +106,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = kubeStore.BatchAdd(context.TODO(), datas2) - equal := cmp.Diff(strings.Contains(err.Error(), "save components occur error"), true) + equal := cmp.Diff(strings.Contains(err.Error(), "save entities occur error"), true) Expect(equal).To(BeEmpty()) }) @@ -157,6 +157,26 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(err).ShouldNot(HaveOccurred()) diff = cmp.Diff(len(list), 4) Expect(diff).Should(BeEmpty()) + + list, err = kubeStore.List(context.TODO(), &app, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{In: []datastore.InQueryOption{ + { + Key: "name", + Values: []string{"kubevela-app-3", "kubevela-app-2"}, + }, + }}}) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 2) + Expect(diff).Should(BeEmpty()) + + list, err = kubeStore.List(context.TODO(), &app, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }}}) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 3) + Expect(diff).Should(BeEmpty()) + }) It("Test list clusters with sort and fuzzy query", func() { @@ -221,6 +241,23 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) Expect(err).Should(Succeed()) Expect(count).Should(Equal(int64(2))) + + count, err = kubeStore.Count(context.TODO(), &app, &datastore.FilterOptions{In: []datastore.InQueryOption{ + { + Key: "name", + Values: []string{"kubevela-app-3", "kubevela-app-2"}, + }, + }}) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(2))) + + count, err = kubeStore.Count(context.TODO(), &app, &datastore.FilterOptions{IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }}) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(3))) }) It("Test isExist function", func() { diff --git a/pkg/apiserver/datastore/mongodb/mongodb.go b/pkg/apiserver/datastore/mongodb/mongodb.go index f40348ac7..28f5d2e59 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb.go +++ b/pkg/apiserver/datastore/mongodb/mongodb.go @@ -84,23 +84,23 @@ func (m *mongodb) Add(ctx context.Context, entity datastore.Entity) error { } // BatchAdd batch add entity, this operation has some atomicity. -func (m *mongodb) BatchAdd(ctx context.Context, entitys []datastore.Entity) error { - donotRollback := make(map[string]int) - for i, saveEntity := range entitys { +func (m *mongodb) BatchAdd(ctx context.Context, entities []datastore.Entity) error { + notRollback := make(map[string]int) + for i, saveEntity := range entities { if err := m.Add(ctx, saveEntity); err != nil { if errors.Is(err, datastore.ErrRecordExist) { - donotRollback[saveEntity.PrimaryKey()] = 1 + notRollback[saveEntity.PrimaryKey()] = 1 } - for _, deleteEntity := range entitys[:i] { - if _, exit := donotRollback[deleteEntity.PrimaryKey()]; !exit { + for _, deleteEntity := range entities[:i] { + if _, exit := notRollback[deleteEntity.PrimaryKey()]; !exit { if err := m.Delete(ctx, deleteEntity); err != nil { if !errors.Is(err, datastore.ErrRecordNotExist) { - log.Logger.Errorf("rollback delete component failure %w", err) + log.Logger.Errorf("rollback delete entity failure %w", err) } } } } - return datastore.NewDBError(fmt.Errorf("save components occur error, %w", err)) + return datastore.NewDBError(fmt.Errorf("save entities occur error, %w", err)) } } return nil @@ -192,10 +192,14 @@ func (m *mongodb) Delete(ctx context.Context, entity datastore.Entity) error { } func _applyFilterOptions(filter bson.D, filterOptions datastore.FilterOptions) bson.D { - if len(filterOptions.Queries) > 0 { - for _, queryOp := range filterOptions.Queries { - filter = append(filter, bson.E{Key: strings.ToLower(queryOp.Key), Value: bsonx.Regex(".*"+queryOp.Query+".*", "s")}) - } + for _, queryOp := range filterOptions.Queries { + filter = append(filter, bson.E{Key: strings.ToLower(queryOp.Key), Value: bsonx.Regex(".*"+queryOp.Query+".*", "s")}) + } + for _, queryOp := range filterOptions.In { + filter = append(filter, bson.E{Key: strings.ToLower(queryOp.Key), Value: bson.D{bson.E{Key: "$in", Value: queryOp.Values}}}) + } + for _, queryOp := range filterOptions.IsNotExist { + filter = append(filter, bson.E{Key: strings.ToLower(queryOp.Key), Value: bson.D{bson.E{Key: "$eq", Value: ""}}}) } return filter } @@ -216,7 +220,7 @@ func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datasto }) } } - if op != nil && len(op.Queries) > 0 { + if op != nil { filter = _applyFilterOptions(filter, op.FilterOptions) } var findOptions options.FindOptions @@ -276,7 +280,7 @@ func (m *mongodb) Count(ctx context.Context, entity datastore.Entity, filterOpti }) } } - if filterOptions != nil && len(filterOptions.Queries) > 0 { + if filterOptions != nil { filter = _applyFilterOptions(filter, *filterOptions) } count, err := collection.CountDocuments(ctx, filter) diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index d0d8615cd..630287076 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -70,7 +70,7 @@ var _ = Describe("Test mongodb datastore driver", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, &model.Application{Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Name: "kubevela-app-4", Project: "test-project", Description: "this is demo 4"}, &model.Workflow{Name: "kubevela-app-workflow", AppPrimaryKey: "kubevela-app-2", Description: "this is workflow"}, &model.ApplicationTrigger{Name: "kubevela-app-trigger", AppPrimaryKey: "kubevela-app-2", Token: "token-test", Description: "this is demo 4"}, } @@ -82,7 +82,7 @@ var _ = Describe("Test mongodb datastore driver", func() { &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = mongodbDriver.BatchAdd(context.TODO(), datas2) - equal := cmp.Diff(strings.Contains(err.Error(), "save components occur error"), true) + equal := cmp.Diff(strings.Contains(err.Error(), "save entities occur error"), true) Expect(equal).To(BeEmpty()) }) @@ -133,6 +133,25 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(err).ShouldNot(HaveOccurred()) diff = cmp.Diff(len(list), 1) Expect(diff).Should(BeEmpty()) + + list, err = mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{In: []datastore.InQueryOption{ + { + Key: "name", + Values: []string{"kubevela-app-3", "kubevela-app-2"}, + }, + }}}) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 2) + Expect(diff).Should(BeEmpty()) + + list, err = mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }}}) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 3) + Expect(diff).Should(BeEmpty()) }) It("Test list clusters with sort and fuzzy query", func() { @@ -196,6 +215,23 @@ var _ = Describe("Test mongodb datastore driver", func() { }) Expect(err).Should(Succeed()) Expect(count).Should(Equal(int64(2))) + + count, err = mongodbDriver.Count(context.TODO(), &app, &datastore.FilterOptions{In: []datastore.InQueryOption{ + { + Key: "name", + Values: []string{"kubevela-app-3", "kubevela-app-2"}, + }, + }}) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(2))) + + count, err = mongodbDriver.Count(context.TODO(), &app, &datastore.FilterOptions{IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }}) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(3))) }) It("Test isExist function", func() { diff --git a/pkg/apiserver/model/project.go b/pkg/apiserver/model/project.go index a9f94bba0..ff86f7dd5 100644 --- a/pkg/apiserver/model/project.go +++ b/pkg/apiserver/model/project.go @@ -25,6 +25,7 @@ type Project struct { BaseModel Name string `json:"name"` Alias string `json:"alias"` + Owner string `json:"owner"` Description string `json:"description,omitempty"` } @@ -49,5 +50,8 @@ func (p *Project) Index() map[string]string { if p.Name != "" { index["name"] = p.Name } + if p.Owner != "" { + index["owner"] = p.Owner + } return index } diff --git a/pkg/apiserver/model/target.go b/pkg/apiserver/model/target.go index e78fa0c86..795fe9ca0 100644 --- a/pkg/apiserver/model/target.go +++ b/pkg/apiserver/model/target.go @@ -26,6 +26,7 @@ type Target struct { BaseModel Name string `json:"name"` Alias string `json:"alias,omitempty"` + Project string `json:"project"` Description string `json:"description,omitempty"` Cluster *ClusterTarget `json:"cluster,omitempty"` Variable map[string]interface{} `json:"variable,omitempty"` @@ -52,6 +53,9 @@ func (d *Target) Index() map[string]string { if d.Name != "" { index["name"] = d.Name } + if d.Project != "" { + index["project"] = d.Project + } return index } diff --git a/pkg/apiserver/model/user.go b/pkg/apiserver/model/user.go index 1f61d4e29..a897b7549 100644 --- a/pkg/apiserver/model/user.go +++ b/pkg/apiserver/model/user.go @@ -27,8 +27,14 @@ import ( func init() { RegisterModel(&User{}) RegisterModel(&ProjectUser{}) + RegisterModel(&Role{}) + RegisterModel(&Permission{}) + RegisterModel(&PermissionTemplate{}) } +// DefaultAdminUserName default admin user name +const DefaultAdminUserName = "admin" + // User is the model of user type User struct { BaseModel @@ -38,6 +44,8 @@ type User struct { Password string `json:"password,omitempty"` Disabled bool `json:"disabled"` LastLoginTime time.Time `json:"lastLoginTime,omitempty"` + // UserRoles binding the platform level roles + UserRoles []string `json:"userRoles"` } // TableName return custom table name @@ -70,9 +78,10 @@ func (u *User) Index() map[string]string { // ProjectUser is the model of user in project type ProjectUser struct { BaseModel - Username string `json:"username"` - ProjectName string `json:"projectName"` - UserRoles []string `json:"userRoles"` + Username string `json:"username"` + ProjectName string `json:"projectName"` + // UserRoles binding the project level roles + UserRoles []string `json:"userRoles"` } // TableName return custom table name @@ -114,3 +123,140 @@ type CustomClaims struct { GrantType string `json:"grantType"` jwt.StandardClaims } + +// Role is a model for a new RBAC mode. +type Role struct { + BaseModel + Name string `json:"name"` + Alias string `json:"alias"` + Project string `json:"project,omitempty"` + Permissions []string `json:"permissions"` +} + +// Permission is a model for a new RBAC mode. +type Permission struct { + BaseModel + Name string `json:"name"` + Alias string `json:"alias"` + Project string `json:"project,omitempty"` + Resources []string `json:"resources"` + Actions []string `json:"actions"` + // Effect option values: Allow,Deny + Effect string `json:"effect"` + Principal *Principal `json:"principal,omitempty"` + Condition *Condition `json:"condition,omitempty"` +} + +// Principal is a model for a new RBAC mode. +type Principal struct { + // Type options: User or Role + Type string `json:"type"` + Names []string `json:"names"` +} + +// Condition is a model for a new RBAC mode. +type Condition struct { +} + +// TableName return custom table name +func (r *Role) TableName() string { + return tableNamePrefix + "role" +} + +// ShortTableName return custom table name +func (r *Role) ShortTableName() string { + return "role" +} + +// PrimaryKey return custom primary key +func (r *Role) PrimaryKey() string { + if r.Project == "" { + return r.Name + } + return fmt.Sprintf("%s-%s", r.Project, r.Name) +} + +// Index return custom index +func (r *Role) Index() map[string]string { + index := make(map[string]string) + if r.Name != "" { + index["name"] = r.Name + } + if r.Project != "" { + index["project"] = r.Project + } + return index +} + +// TableName return custom table name +func (p *Permission) TableName() string { + return tableNamePrefix + "perm" +} + +// ShortTableName return custom table name +func (p *Permission) ShortTableName() string { + return "perm" +} + +// PrimaryKey return custom primary key +func (p *Permission) PrimaryKey() string { + if p.Project == "" { + return p.Name + } + return fmt.Sprintf("%s-%s", p.Project, p.Name) +} + +// Index return custom index +func (p *Permission) Index() map[string]string { + index := make(map[string]string) + if p.Name != "" { + index["name"] = p.Name + } + if p.Project != "" { + index["project"] = p.Project + } + if p.Principal != nil && p.Principal.Type != "" { + index["principal.type"] = p.Principal.Type + } + return index +} + +// PermissionTemplate is a model for a new RBAC mode. +type PermissionTemplate struct { + BaseModel + Name string `json:"name"` + Alias string `json:"alias"` + // Scope options: project or platform + Scope string `json:"scope"` + Resources []string `json:"resources"` + Actions []string `json:"actions"` + Effect string `json:"effect"` + Condition *Condition `json:"condition,omitempty"` +} + +// TableName return custom table name +func (p *PermissionTemplate) TableName() string { + return tableNamePrefix + "perm_temp" +} + +// ShortTableName return custom table name +func (p *PermissionTemplate) ShortTableName() string { + return "perm_temp" +} + +// PrimaryKey return custom primary key +func (p *PermissionTemplate) PrimaryKey() string { + return p.Name +} + +// Index return custom index +func (p *PermissionTemplate) Index() map[string]string { + index := make(map[string]string) + if p.Name != "" { + index["name"] = p.Name + } + if p.Scope != "" { + index["scope"] = p.Scope + } + return index +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 07ef01e3f..1fffedb17 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -289,10 +289,10 @@ type ClusterBase struct { // ListApplicationOptions list application query options type ListApplicationOptions struct { - Project string `json:"project"` - Env string `json:"env"` - TargetName string `json:"targetName"` - Query string `json:"query"` + Projects []string `json:"projects"` + Env string `json:"env"` + TargetName string `json:"targetName"` + Query string `json:"query"` } // ListApplicationResponse list applications by query params @@ -678,6 +678,7 @@ type ProjectBase struct { Description string `json:"description"` CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` + Owner NameAlias `json:"owner,omitempty"` } // CreateProjectRequest create project request body @@ -685,6 +686,14 @@ type CreateProjectRequest struct { Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description" optional:"true"` + Owner string `json:"owner" optional:"true"` +} + +// UpdateProjectRequest update a project request body +type UpdateProjectRequest struct { + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Owner string `json:"owner" optional:"true"` } // Env models the data of env in API @@ -989,6 +998,7 @@ type ApplicationTrait struct { type CreateTargetRequest struct { Name string `json:"name" validate:"checkname"` Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Project string `json:"project" validate:"checkname"` Description string `json:"description,omitempty" optional:"true"` Cluster *ClusterTarget `json:"cluster,omitempty"` Variable map[string]interface{} `json:"variable,omitempty"` @@ -1029,6 +1039,7 @@ type TargetBase struct { CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` AppNum int64 `json:"appNum,omitempty"` + Project NameAlias `json:"project"` } // ApplicationRevisionBase application revision base spec @@ -1118,29 +1129,39 @@ type DexConfigResponse struct { // DetailUserResponse is the response of user detail type DetailUserResponse struct { UserBase - Projects []ProjectUserBase `json:"projects"` + Projects []*ProjectBase `json:"projects"` + Roles []NameAlias `json:"roles"` } // ProjectUserBase project user base type ProjectUserBase struct { - Name string `json:"name"` - Alias string `json:"alias"` - UserRoles []string `json:"userRoles"` + UserName string `json:"name"` + UserRoles []string `json:"userRoles"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// ListProjectUsersResponse the response body that list users belong to a project +type ListProjectUsersResponse struct { + Users []*ProjectUserBase `json:"users"` + Total int64 `json:"total"` } // CreateUserRequest create user request type CreateUserRequest struct { - Name string `json:"name" validate:"checkname"` - Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` - Email string `json:"email" validate:"checkemail"` - Password string `json:"password" validate:"checkpassword"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Email string `json:"email" validate:"checkemail"` + Password string `json:"password" validate:"checkpassword"` + Roles []string `json:"roles"` } // UpdateUserRequest update user request type UpdateUserRequest struct { - Alias string `json:"alias,omitempty" optional:"true"` - Password string `json:"password,omitempty" validate:"checkpassword" optional:"true"` - Email string `json:"email,omitempty" validate:"checkemail" optional:"true"` + Alias string `json:"alias,omitempty" optional:"true"` + Password string `json:"password,omitempty" validate:"checkpassword" optional:"true"` + Email string `json:"email,omitempty" validate:"checkemail" optional:"true"` + Roles *[]string `json:"roles"` } // ListUserResponse list user response @@ -1170,3 +1191,80 @@ type ListUserOptions struct { type GetLoginTypeResponse struct { LoginType string `json:"loginType"` } + +// AddProjectUserRequest the request body that add user to project +type AddProjectUserRequest struct { + UserName string `json:"userName" validate:"checkname"` + UserRoles []string `json:"userRoles"` +} + +// UpdateProjectUserRequest the request body that update user role in a project +type UpdateProjectUserRequest struct { + UserRoles []string `json:"userRoles"` +} + +// CreateRoleRequest the request body that create a role +type CreateRoleRequest struct { + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` + Permissions []string `json:"permissions"` +} + +// UpdateRoleRequest the request body that update a role +type UpdateRoleRequest struct { + Alias string `json:"alias" validate:"checkalias"` + Permissions []string `json:"permissions"` +} + +// RoleBase the base struct of role +type RoleBase struct { + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + Name string `json:"name"` + Alias string `json:"alias,omitempty"` + Permissions []NameAlias `json:"permissions"` +} + +// ListRolesResponse the response body of list roles +type ListRolesResponse struct { + Total int64 `json:"total"` + Roles []*RoleBase `json:"roles"` +} + +// PermissionTemplateBase the perm policy template base struct +type PermissionTemplateBase struct { + Name string `json:"name"` + Alias string `json:"alias"` + Resources []string `json:"resources"` + Actions []string `json:"actions"` + Effect string `json:"effect"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// PermissionBase the perm policy base struct +type PermissionBase struct { + Name string `json:"name"` + Alias string `json:"alias"` + Resources []string `json:"resources"` + Actions []string `json:"actions"` + Effect string `json:"effect"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// UpdatePermissionRequest the request body that update permission policy +type UpdatePermissionRequest struct { + Alias string `json:"alias" validate:"checkalias"` + Resources []string `json:"resources"` + Actions []string `json:"actions"` + Effect string `json:"effect" validate:"oneof=Allow Deny"` +} + +// LoginUserInfoResponse the response body of login user info +type LoginUserInfoResponse struct { + UserBase + Projects []*ProjectBase `json:"projects"` + PlatformPermissions []PermissionBase `json:"platformPermissions"` + ProjectPermissions map[string][]PermissionBase `json:"projectPermissions"` +} diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index d306358c7..37b2b5dec 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -18,7 +18,6 @@ package rest import ( "context" - "errors" "fmt" "net/http" "os" @@ -27,22 +26,16 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/emicklei/go-restful/v3" "github.com/go-openapi/spec" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/datastore/kubeapi" "github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb" "github.com/oam-dev/kubevela/pkg/apiserver/log" - "github.com/oam-dev/kubevela/pkg/apiserver/model" "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/webservice" @@ -78,13 +71,15 @@ type leaderConfig struct { // APIServer interface for call api server type APIServer interface { Run(context.Context) error - RegisterServices() restfulspec.Config + RegisterServices(ctx context.Context, initDatabase bool) restfulspec.Config } type restServer struct { webContainer *restful.Container cfg Config dataStore datastore.DataStore + // usecases, we register part of the usecase instances + usecases map[string]interface{} } // New create restserver with config data @@ -114,11 +109,7 @@ func New(cfg Config) (a APIServer, err error) { } func (s *restServer) Run(ctx context.Context) error { - s.RegisterServices() - - if err := s.InitAdmin(ctx); err != nil { - return err - } + s.RegisterServices(ctx, true) l, err := s.setupLeaderElection() if err != nil { @@ -149,7 +140,7 @@ func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig RetryPeriod: time.Second * 2, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { - go velasync.Start(ctx, s.dataStore, restCfg) + go velasync.Start(ctx, s.dataStore, restCfg, s.usecases) s.runWorkflowRecordSync(ctx, s.cfg.LeaderConfig.Duration) }, OnStoppedLeading: func() { @@ -169,10 +160,9 @@ func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig }, nil } -func (s restServer) runWorkflowRecordSync(ctx context.Context, duration time.Duration) { +func (s *restServer) runWorkflowRecordSync(ctx context.Context, duration time.Duration) { klog.Infof("start to syncing workflow record") - w := usecase.NewWorkflowUsecase(s.dataStore, usecase.NewEnvUsecase(s.dataStore)) - + w := s.usecases["workflow"].(usecase.WorkflowUsecase) t := time.NewTicker(duration) defer t.Stop() @@ -189,8 +179,9 @@ func (s restServer) runWorkflowRecordSync(ctx context.Context, duration time.Dur } // RegisterServices register web service -func (s *restServer) RegisterServices() restfulspec.Config { - webservice.Init(s.dataStore, s.cfg.AddonCacheTime) +func (s *restServer) RegisterServices(ctx context.Context, initDatabase bool) restfulspec.Config { + s.usecases = webservice.Init(ctx, s.dataStore, s.cfg.AddonCacheTime, initDatabase) + /* ************************************************************** */ /* ************* Open API Route Group ***************** */ /* ************************************************************** */ @@ -223,57 +214,6 @@ func (s *restServer) RegisterServices() restfulspec.Config { return config } -// RegisterServices register web service -func (s *restServer) InitAdmin(ctx context.Context) error { - admin := "admin" - if err := s.dataStore.Get(ctx, &model.User{ - Name: admin, - }); err != nil { - if errors.Is(err, datastore.ErrRecordNotExist) { - pwd := utils2.RandomString(8) - encrypted, err := usecase.GeneratePasswordHash(pwd) - if err != nil { - return err - } - if err := s.dataStore.Add(ctx, &model.User{ - Name: admin, - Password: encrypted, - }); err != nil { - return err - } - kubecli, err := clients.GetKubeClient() - if err != nil { - log.Logger.Fatalf("failed to get kube client: %s", err.Error()) - return err - } - secret := &corev1.Secret{} - if err := kubecli.Get(ctx, k8stypes.NamespacedName{ - Name: admin, - Namespace: types.DefaultKubeVelaNS, - }, secret); err != nil { - if apierrors.IsNotFound(err) { - if err := kubecli.Create(ctx, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: admin, - Namespace: types.DefaultKubeVelaNS, - }, - StringData: map[string]string{ - admin: pwd, - }, - }); err != nil { - return err - } - } else { - return err - } - } - } else { - return err - } - } - return nil -} - func (s *restServer) requestLog(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { start := time.Now() c := utils.NewResponseCapture(resp.ResponseWriter) diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 50ad1c422..e5b9d597d 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -48,7 +48,7 @@ import ( apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" - "github.com/oam-dev/kubevela/pkg/apiserver/sync" + syncconvert "github.com/oam-dev/kubevela/pkg/apiserver/sync/convert" "github.com/oam-dev/kubevela/pkg/appfile/dryrun" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" @@ -130,7 +130,7 @@ func NewApplicationUsecase(ds datastore.DataStore, ) ApplicationUsecase { kubecli, err := clients.GetKubeClient() if err != nil { - log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + log.Logger.Fatalf("get kube client failure %s", err.Error()) } return &applicationUsecaseImpl{ ds: ds, @@ -147,9 +147,6 @@ func NewApplicationUsecase(ds datastore.DataStore, func listApp(ctx context.Context, ds datastore.DataStore, listOptions apisv1.ListApplicationOptions) ([]*model.Application, error) { var app = model.Application{} - if listOptions.Project != "" { - app.Project = listOptions.Project - } var err error var envBinding []*apisv1.EnvBindingBase if listOptions.Env != "" || listOptions.TargetName != "" { @@ -159,8 +156,14 @@ func listApp(ctx context.Context, ds datastore.DataStore, listOptions apisv1.Lis return nil, err } } - - entities, err := ds.List(ctx, &app, &datastore.ListOptions{}) + var filterOptions datastore.FilterOptions + if len(listOptions.Projects) > 0 { + filterOptions.In = append(filterOptions.In, datastore.InQueryOption{ + Key: "project", + Values: listOptions.Projects, + }) + } + entities, err := ds.List(ctx, &app, &datastore.ListOptions{FilterOptions: filterOptions}) if err != nil { return nil, err } @@ -202,13 +205,36 @@ func listApp(ctx context.Context, ds datastore.DataStore, listOptions apisv1.Lis // ListApplications list applications func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicationOptions) ([]*apisv1.ApplicationBase, error) { + userName, ok := ctx.Value(&apisv1.CtxKeyUser).(string) + if !ok { + return nil, bcode.ErrUnauthorized + } + projects, err := c.projectUsecase.ListUserProjects(ctx, userName) + if err != nil { + return nil, err + } + var availableProjectNames []string + for _, project := range projects { + availableProjectNames = append(availableProjectNames, project.Name) + } + if len(availableProjectNames) == 0 { + return []*apisv1.ApplicationBase{}, nil + } + if len(listOptions.Projects) > 0 { + if !utils2.SliceIncludeSlice(availableProjectNames, listOptions.Projects) { + return []*apisv1.ApplicationBase{}, nil + } + } + if len(listOptions.Projects) == 0 { + listOptions.Projects = availableProjectNames + } apps, err := listApp(ctx, c.ds, listOptions) if err != nil { return nil, err } var list []*apisv1.ApplicationBase for _, app := range apps { - appBase := c.convertAppModelToBase(ctx, app) + appBase := c.convertAppModelToBase(app, projects) list = append(list, appBase) } sort.Slice(list, func(i, j int) bool { @@ -233,8 +259,16 @@ func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName str // DetailApplication detail application info func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { - base := c.convertAppModelToBase(ctx, app) - policys, err := c.queryApplicationPolicies(ctx, app) + var project *apisv1.ProjectBase + if app.Project != "" { + var err error + project, err = c.projectUsecase.DetailProject(ctx, app.Project) + if err != nil { + return nil, bcode.ErrProjectIsNotExist + } + } + base := c.convertAppModelToBase(app, []*apisv1.ProjectBase{project}) + policies, err := c.queryApplicationPolicies(ctx, app) if err != nil { return nil, err } @@ -248,7 +282,7 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod } var policyNames []string var envBindingNames []string - for _, p := range policys { + for _, p := range policies { policyNames = append(policyNames, p.Name) } for _, e := range envBindings { @@ -349,9 +383,9 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, bcode.ErrApplicationExist } // check project - project, err := c.projectUsecase.GetProject(ctx, req.Project) + project, err := c.projectUsecase.DetailProject(ctx, req.Project) if err != nil { - return nil, err + return nil, bcode.ErrProjectIsNotExist } application.Project = project.Name @@ -385,7 +419,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } // render app base info. - base := c.convertAppModelToBase(ctx, &application) + base := c.convertAppModelToBase(&application, []*apisv1.ProjectBase{project}) return base, nil } @@ -506,6 +540,14 @@ func (c *applicationUsecaseImpl) saveApplicationEnvBinding(ctx context.Context, } func (c *applicationUsecaseImpl) UpdateApplication(ctx context.Context, app *model.Application, req apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) { + var project *apisv1.ProjectBase + if app.Project != "" { + var err error + project, err = c.projectUsecase.DetailProject(ctx, app.Project) + if err != nil { + return nil, bcode.ErrProjectIsNotExist + } + } app.Alias = req.Alias app.Description = req.Description app.Labels = req.Labels @@ -513,7 +555,7 @@ func (c *applicationUsecaseImpl) UpdateApplication(ctx context.Context, app *mod if err := c.ds.Put(ctx, app); err != nil { return nil, err } - return c.convertAppModelToBase(ctx, app), nil + return c.convertAppModelToBase(app, []*apisv1.ProjectBase{project}), nil } // ListRecords list application record @@ -617,11 +659,11 @@ func (c *applicationUsecaseImpl) queryApplicationPolicies(ctx context.Context, a var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), } - policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) + policies, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) if err != nil { return nil, err } - for _, policy := range policys { + for _, policy := range policies { pm := policy.(*model.ApplicationPolicy) list = append(list, pm) } @@ -893,7 +935,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return app, nil } -func (c *applicationUsecaseImpl) convertAppModelToBase(ctx context.Context, app *model.Application) *apisv1.ApplicationBase { +func (c *applicationUsecaseImpl) convertAppModelToBase(app *model.Application, projects []*apisv1.ProjectBase) *apisv1.ApplicationBase { appBase := &apisv1.ApplicationBase{ Name: app.Name, Alias: app.Alias, @@ -902,17 +944,15 @@ func (c *applicationUsecaseImpl) convertAppModelToBase(ctx context.Context, app Description: app.Description, Icon: app.Icon, Labels: app.Labels, + Project: &apisv1.ProjectBase{Name: app.Project}, } if app.IsSynced() { appBase.ReadOnly = true } - - project, err := c.projectUsecase.GetProject(ctx, app.Project) - if err != nil { - log.Logger.Errorf("query project info failure %s", err.Error()) - } - if project != nil { - appBase.Project = convertProjectModel2Base(project) + for _, project := range projects { + if project.Name == app.Project { + appBase.Project = project + } } return appBase } @@ -1610,7 +1650,7 @@ func (c *applicationUsecaseImpl) resetApp(ctx context.Context, targetApp *v1beta for _, comp := range targetComps { // add or update new app's components from old app if utils.StringsContain(readyToAdd, comp.Name) || utils.StringsContain(readyToUpdate, comp.Name) { - compModel, err := sync.ConvertFromCRComponent(appPrimaryKey, comp) + compModel, err := syncconvert.FromCRComponent(appPrimaryKey, comp) if err != nil { return &apisv1.AppResetResponse{}, bcode.ErrInvalidProperties } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 1c7297776..195c25f05 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -42,11 +42,13 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/apply" ) var _ = Describe("Test application usecase function", func() { var ( + rbacUsecase *rbacUsecaseImpl appUsecase *applicationUsecaseImpl workflowUsecase *workflowUsecaseImpl envUsecase *envUsecaseImpl @@ -54,6 +56,7 @@ var _ = Describe("Test application usecase function", func() { targetUsecase *targetUsecaseImpl definitionUsecase *definitionUsecaseImpl projectUsecase *projectUsecaseImpl + userUsecase *userUsecaseImpl testProject = "app-project" testApp = "test-app" defaultTarget = "default" @@ -66,12 +69,14 @@ var _ = Describe("Test application usecase function", func() { ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "app-test-kubevela"}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient} + rbacUsecase = &rbacUsecaseImpl{ds: ds} + userUsecase = &userUsecaseImpl{ds: ds, k8sClient: k8sClient} + projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} + envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient, projectUsecase: projectUsecase} workflowUsecase = &workflowUsecaseImpl{ds: ds, envUsecase: envUsecase} definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient, caches: utils.NewMemoryCacheStore(context.Background())} envBindingUsecase = &envBindingUsecaseImpl{ds: ds, envUsecase: envUsecase, workflowUsecase: workflowUsecase, kubeClient: k8sClient, definitionUsecase: definitionUsecase} targetUsecase = &targetUsecaseImpl{ds: ds, k8sClient: k8sClient} - projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient} appUsecase = &applicationUsecaseImpl{ ds: ds, workflowUsecase: workflowUsecase, @@ -87,11 +92,19 @@ var _ = Describe("Test application usecase function", func() { It("Test CreateApplication function", func() { - By("test sample create") - _, err := targetUsecase.CreateTarget(context.TODO(), v1.CreateTargetRequest{Name: defaultTarget, Cluster: &v1.ClusterTarget{ClusterName: "local", Namespace: namespace1}}) + By("init default admin user") + var ns = corev1.Namespace{} + ns.Name = types.DefaultKubeVelaNS + err := k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + err = userUsecase.Init(context.TODO()) Expect(err).Should(BeNil()) - _, err = projectUsecase.CreateProject(context.TODO(), v1.CreateProjectRequest{Name: testProject}) + By("prepare test project") + _, err = projectUsecase.CreateProject(context.TODO(), v1.CreateProjectRequest{Name: testProject, Owner: model.DefaultAdminUserName}) + Expect(err).Should(BeNil()) + + _, err = targetUsecase.CreateTarget(context.TODO(), v1.CreateTargetRequest{Name: defaultTarget, Project: testProject, Cluster: &v1.ClusterTarget{ClusterName: "local", Namespace: namespace1}}) Expect(err).Should(BeNil()) _, err = envUsecase.CreateEnv(context.TODO(), v1.CreateEnvRequest{Name: "app-dev", Namespace: envnsdev, Targets: []string{defaultTarget}, Project: "app-dev"}) @@ -114,6 +127,7 @@ var _ = Describe("Test application usecase function", func() { Properties: "{\"image\":\"nginx\"}", }, } + By("test create application") base, err := appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) @@ -124,13 +138,13 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListApplications function", func() { - _, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicationOptions{}) + _, err := appUsecase.ListApplications(context.WithValue(context.TODO(), &v1.CtxKeyUser, model.DefaultAdminUserName), v1.ListApplicationOptions{}) Expect(err).Should(BeNil()) }) It("Test ListApplications and filter by targetName function", func() { - list, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicationOptions{ - Project: testProject, + list, err := appUsecase.ListApplications(context.WithValue(context.TODO(), &v1.CtxKeyUser, model.DefaultAdminUserName), v1.ListApplicationOptions{ + Projects: []string{testProject}, TargetName: defaultTarget}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(list), 1)).Should(BeEmpty()) @@ -481,7 +495,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(compareResponse.IsDiff, false)).Should(BeEmpty()) By("compare when app's env add target, should return true") - _, err = targetUsecase.CreateTarget(context.TODO(), v1.CreateTargetRequest{Name: "dev-target1", Cluster: &v1.ClusterTarget{ClusterName: "local", Namespace: "dev-target1"}}) + _, err = targetUsecase.CreateTarget(context.TODO(), v1.CreateTargetRequest{Name: "dev-target1", Project: appModel.Project, Cluster: &v1.ClusterTarget{ClusterName: "local", Namespace: "dev-target1"}}) Expect(err).Should(BeNil()) _, err = envUsecase.UpdateEnv(context.TODO(), "app-dev", v1.UpdateEnvRequest{ @@ -594,8 +608,9 @@ var _ = Describe("Test application component usecase function", func() { ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "app-test-kubevela"}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient} - envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} + envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient, projectUsecase: projectUsecase} workflowUsecase := &workflowUsecaseImpl{ds: ds, envUsecase: envUsecase} envBindingUsecase := &envBindingUsecaseImpl{ds: ds, envUsecase: envUsecase, workflowUsecase: workflowUsecase, kubeClient: k8sClient} diff --git a/pkg/apiserver/rest/usecase/authentication.go b/pkg/apiserver/rest/usecase/authentication.go index 5b32abd86..8bb6ccecd 100644 --- a/pkg/apiserver/rest/usecase/authentication.go +++ b/pkg/apiserver/rest/usecase/authentication.go @@ -27,6 +27,7 @@ import ( "github.com/form3tech-oss/jwt-go" "golang.org/x/oauth2" v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -172,6 +173,9 @@ func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.L if err != nil { return nil, err } + if userBase.Disabled { + return nil, bcode.ErrUserAlreadyDisabled + } accessToken, err := a.generateJWTToken(userBase.Name, GrantTypeAccess, time.Hour) if err != nil { return nil, err @@ -255,6 +259,9 @@ func (a *authenticationUsecaseImpl) GetDexConfig(ctx context.Context) (*apisv1.D Name: secretDexConfig, Namespace: velatypes.DefaultKubeVelaNS, }, secret); err != nil { + if apierrors.IsNotFound(err) { + return nil, bcode.ErrDexConfigNotFound + } log.Logger.Errorf("failed to get dex config: %s", err.Error()) return nil, err } @@ -287,8 +294,12 @@ func (a *authenticationUsecaseImpl) GetLoginType(ctx context.Context) (*apisv1.G if err != nil { return nil, err } + loginType := sysInfo.LoginType + if loginType == "" { + loginType = model.LoginTypeLocal + } return &apisv1.GetLoginTypeResponse{ - LoginType: sysInfo.LoginType, + LoginType: loginType, }, nil } @@ -337,7 +348,7 @@ func (l *localHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) if err := compareHashWithPassword(user.Password, l.password); err != nil { return nil, err } - if err := l.userUsecase.updateUserLoginTime(ctx, user); err != nil { + if err := l.userUsecase.UpdateUserLoginTime(ctx, user); err != nil { return nil, err } return &apisv1.UserBase{ diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go index bc0e9d2ee..62373c794 100644 --- a/pkg/apiserver/rest/usecase/definition_test.go +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -179,11 +179,6 @@ var _ = Describe("Test namespace usecase functions", func() { Expect(cmp.Diff(len(uiSchema), 12)).Should(BeEmpty()) Expect(cmp.Diff(uiSchema[7].JSONKey, "livenessProbe")).Should(BeEmpty()) Expect(cmp.Diff(len(uiSchema[7].SubParameters), 8)).Should(BeEmpty()) - - outdata, err := yaml.Marshal(uiSchema) - Expect(err).Should(Succeed()) - err = ioutil.WriteFile("./testdata/ui-schema.yaml", outdata, 0755) - Expect(err).Should(Succeed()) }) It("Test sortDefaultUISchema", testSortDefaultUISchema) diff --git a/pkg/apiserver/rest/usecase/env.go b/pkg/apiserver/rest/usecase/env.go index 43e1854cd..debbb03dc 100644 --- a/pkg/apiserver/rest/usecase/env.go +++ b/pkg/apiserver/rest/usecase/env.go @@ -39,23 +39,25 @@ import ( type EnvUsecase interface { GetEnv(ctx context.Context, envName string) (*model.Env, error) ListEnvs(ctx context.Context, page, pageSize int, listOption apisv1.ListEnvOptions) (*apisv1.ListEnvResponse, error) + ListEnvCount(ctx context.Context, listOption apisv1.ListEnvOptions) (int64, error) DeleteEnv(ctx context.Context, envName string) error CreateEnv(ctx context.Context, req apisv1.CreateEnvRequest) (*apisv1.Env, error) UpdateEnv(ctx context.Context, envName string, req apisv1.UpdateEnvRequest) (*apisv1.Env, error) } type envUsecaseImpl struct { - ds datastore.DataStore - kubeClient client.Client + ds datastore.DataStore + projectUsecase ProjectUsecase + kubeClient client.Client } // NewEnvUsecase new env usecase -func NewEnvUsecase(ds datastore.DataStore) EnvUsecase { +func NewEnvUsecase(ds datastore.DataStore, projectUsecase ProjectUsecase) EnvUsecase { kubecli, err := clients.GetKubeClient() if err != nil { log.Logger.Fatalf("get kubeclient failure %s", err.Error()) } - return &envUsecaseImpl{kubeClient: kubecli, ds: ds} + return &envUsecaseImpl{kubeClient: kubecli, ds: ds, projectUsecase: projectUsecase} } // GetEnv get env @@ -96,40 +98,75 @@ func (p *envUsecaseImpl) DeleteEnv(ctx context.Context, envName string) error { // ListEnvs list envs func (p *envUsecaseImpl) ListEnvs(ctx context.Context, page, pageSize int, listOption apisv1.ListEnvOptions) (*apisv1.ListEnvResponse, error) { - entities, err := listEnvs(ctx, p.ds, listOption.Project, &datastore.ListOptions{Page: page, PageSize: pageSize, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) + userName, ok := ctx.Value(&apisv1.CtxKeyUser).(string) + if !ok { + return nil, bcode.ErrUnauthorized + } + projects, err := p.projectUsecase.ListUserProjects(ctx, userName) + if err != nil { + return nil, err + } + var availableProjectNames []string + var projectNameAlias = make(map[string]string) + for _, project := range projects { + availableProjectNames = append(availableProjectNames, project.Name) + projectNameAlias[project.Name] = project.Alias + } + if len(availableProjectNames) == 0 { + return &apisv1.ListEnvResponse{Envs: []*apisv1.Env{}, Total: 0}, nil + } + if listOption.Project != "" { + if !util.StringsContain(availableProjectNames, listOption.Project) { + return &apisv1.ListEnvResponse{Envs: []*apisv1.Env{}, Total: 0}, nil + } + } + projectNames := []string{listOption.Project} + if listOption.Project == "" { + projectNames = availableProjectNames + } + filter := datastore.FilterOptions{ + In: []datastore.InQueryOption{ + { + Key: "project", + Values: projectNames, + }, + }, + } + entities, err := listEnvs(ctx, p.ds, &datastore.ListOptions{ + Page: page, + PageSize: pageSize, + SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: filter, + }) if err != nil { return nil, err } - Targets, err := listTarget(ctx, p.ds, nil) + targets, err := listTarget(ctx, p.ds, listOption.Project, nil) if err != nil { return nil, err } var envs []*apisv1.Env for _, ee := range entities { - envs = append(envs, convertEnvModel2Base(ee, Targets)) + envs = append(envs, convertEnvModel2Base(ee, targets)) } - projectResp, err := listProjects(ctx, p.ds, 0, 0) - if err != nil { - return nil, err + for i := range envs { + envs[i].Project.Alias = projectNameAlias[envs[i].Project.Name] } - for _, e := range envs { - for _, pj := range projectResp.Projects { - if e.Project.Name == pj.Name { - e.Project.Alias = pj.Alias - break - } - } - } - total, err := p.ds.Count(ctx, &model.Env{Project: listOption.Project}, nil) + + total, err := p.ds.Count(ctx, &model.Env{Project: listOption.Project}, &filter) if err != nil { return nil, err } return &apisv1.ListEnvResponse{Envs: envs, Total: total}, nil } +func (p *envUsecaseImpl) ListEnvCount(ctx context.Context, listOption apisv1.ListEnvOptions) (int64, error) { + return p.ds.Count(ctx, &model.Env{Project: listOption.Project}, nil) +} + func checkEqual(old, new []string) bool { if old == nil && new == nil { return true @@ -186,7 +223,7 @@ func (p *envUsecaseImpl) UpdateEnv(ctx context.Context, name string, req apisv1. env.Targets = req.Targets } - targets, err := listTarget(ctx, p.ds, nil) + targets, err := listTarget(ctx, p.ds, "", nil) if err != nil { return nil, err } @@ -232,7 +269,7 @@ func (p *envUsecaseImpl) CreateEnv(ctx context.Context, req apisv1.CreateEnvRequ return nil, bcode.ErrEnvTargetConflict } - targets, err := listTarget(ctx, p.ds, nil) + targets, err := listTarget(ctx, p.ds, "", nil) if err != nil { return nil, err } diff --git a/pkg/apiserver/rest/usecase/env_model.go b/pkg/apiserver/rest/usecase/env_model.go index 8b815ba34..7abd4454d 100644 --- a/pkg/apiserver/rest/usecase/env_model.go +++ b/pkg/apiserver/rest/usecase/env_model.go @@ -79,11 +79,8 @@ func getEnv(ctx context.Context, ds datastore.DataStore, envName string) (*model return env, nil } -func listEnvs(ctx context.Context, ds datastore.DataStore, project string, listOption *datastore.ListOptions) ([]*model.Env, error) { +func listEnvs(ctx context.Context, ds datastore.DataStore, listOption *datastore.ListOptions) ([]*model.Env, error) { var env = model.Env{} - if project != "" { - env.Project = project - } entities, err := ds.List(ctx, &env, listOption) if err != nil { return nil, err diff --git a/pkg/apiserver/rest/usecase/env_test.go b/pkg/apiserver/rest/usecase/env_test.go index ff4392bea..41aee9497 100644 --- a/pkg/apiserver/rest/usecase/env_test.go +++ b/pkg/apiserver/rest/usecase/env_test.go @@ -43,7 +43,9 @@ var _ = Describe("Test env usecase functions", func() { ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "env-test-kubevela"}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - envUsecase = &envUsecaseImpl{kubeClient: k8sClient, ds: ds} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase := &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} + envUsecase = &envUsecaseImpl{kubeClient: k8sClient, ds: ds, projectUsecase: projectUsecase} }) It("Test Create/Get/Delete Env function", func() { // create target @@ -111,7 +113,7 @@ var _ = Describe("Test env usecase functions", func() { Expect(err).Should(BeNil()) By("Test ListEnvs function") - _, err = envUsecase.ListEnvs(context.TODO(), 1, 1, apisv1.ListEnvOptions{}) + _, err = envUsecase.ListEnvs(context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "admin"), 1, 1, apisv1.ListEnvOptions{}) Expect(err).Should(BeNil()) }) diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index 949069334..0bbee7482 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -95,15 +95,26 @@ func pickEnv(envs []*model.Env, name string) (*model.Env, error) { func listFullEnvBinding(ctx context.Context, ds datastore.DataStore, option envListOption) ([]*apisv1.EnvBindingBase, error) { envBindings, err := listEnvBindings(ctx, ds, option) if err != nil { - return nil, bcode.ErrEnvBindingsNotExist } - targets, err := listTarget(ctx, ds, nil) + targets, err := listTarget(ctx, ds, "", nil) if err != nil { return nil, err } - // TODO: list by project - envs, err := listEnvs(ctx, ds, "", nil) + var listOption *datastore.ListOptions + if option.projectName != "" { + listOption = &datastore.ListOptions{ + FilterOptions: datastore.FilterOptions{ + In: []datastore.InQueryOption{ + { + Key: "project", + Values: []string{option.projectName}, + }, + }, + }, + } + } + envs, err := listEnvs(ctx, ds, listOption) if err != nil { return nil, err } @@ -120,7 +131,7 @@ func listFullEnvBinding(ctx context.Context, ds datastore.DataStore, option envL } func (e *envBindingUsecaseImpl) GetEnvBindings(ctx context.Context, app *model.Application) ([]*apisv1.EnvBindingBase, error) { - full, err := listFullEnvBinding(ctx, e.ds, envListOption{appPrimaryKey: app.PrimaryKey()}) + full, err := listFullEnvBinding(ctx, e.ds, envListOption{appPrimaryKey: app.PrimaryKey(), projectName: app.Project}) if err != nil { log.Logger.Errorf("list envbinding for app %s err: %v\n", app.Name, err) return nil, err @@ -306,7 +317,7 @@ func (e *envBindingUsecaseImpl) deleteEnvWorkflow(ctx context.Context, app *mode } func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model.Application, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) { - targets, err := listTarget(ctx, e.ds, nil) + targets, err := listTarget(ctx, e.ds, "", nil) if err != nil { return nil, err } diff --git a/pkg/apiserver/rest/usecase/envbinding_model.go b/pkg/apiserver/rest/usecase/envbinding_model.go index ffd0496c6..6d6008871 100644 --- a/pkg/apiserver/rest/usecase/envbinding_model.go +++ b/pkg/apiserver/rest/usecase/envbinding_model.go @@ -26,6 +26,7 @@ import ( type envListOption struct { appPrimaryKey string envName string + projectName string } func listEnvBindings(ctx context.Context, ds datastore.DataStore, listOption envListOption) ([]*model.EnvBinding, error) { diff --git a/pkg/apiserver/rest/usecase/envbinding_test.go b/pkg/apiserver/rest/usecase/envbinding_test.go index 65325e4b8..29c64fbd7 100644 --- a/pkg/apiserver/rest/usecase/envbinding_test.go +++ b/pkg/apiserver/rest/usecase/envbinding_test.go @@ -48,7 +48,9 @@ var _ = Describe("Test envBindingUsecase functions", func() { testApp = &model.Application{ Name: "test-app-env", } - envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase := &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} + envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient, projectUsecase: projectUsecase} workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient, envUsecase: envUsecase} definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient, caches: utils.NewMemoryCacheStore(context.TODO())} envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase, definitionUsecase: definitionUsecase, kubeClient: k8sClient, envUsecase: envUsecase} diff --git a/pkg/apiserver/rest/usecase/project.go b/pkg/apiserver/rest/usecase/project.go index ca66e444d..641b269e0 100644 --- a/pkg/apiserver/rest/usecase/project.go +++ b/pkg/apiserver/rest/usecase/project.go @@ -19,6 +19,7 @@ package usecase import ( "context" "errors" + "fmt" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,44 +35,80 @@ import ( // ProjectUsecase project manage usecase. type ProjectUsecase interface { GetProject(ctx context.Context, projectName string) (*model.Project, error) + DetailProject(ctx context.Context, projectName string) (*apisv1.ProjectBase, error) ListProjects(ctx context.Context, page, pageSize int) (*apisv1.ListProjectResponse, error) + ListUserProjects(ctx context.Context, userName string) ([]*apisv1.ProjectBase, error) CreateProject(ctx context.Context, req apisv1.CreateProjectRequest) (*apisv1.ProjectBase, error) + DeleteProject(ctx context.Context, projectName string) error + UpdateProject(ctx context.Context, projectName string, req apisv1.UpdateProjectRequest) (*apisv1.ProjectBase, error) + ListProjectUser(ctx context.Context, projectName string, page, pageSize int) (*apisv1.ListProjectUsersResponse, error) + AddProjectUser(ctx context.Context, projectName string, req apisv1.AddProjectUserRequest) (*apisv1.ProjectUserBase, error) + DeleteProjectUser(ctx context.Context, projectName string, userName string) error + UpdateProjectUser(ctx context.Context, projectName string, userName string, req apisv1.UpdateProjectUserRequest) (*apisv1.ProjectUserBase, error) + Init(ctx context.Context) error } type projectUsecaseImpl struct { - ds datastore.DataStore - k8sClient client.Client + ds datastore.DataStore + k8sClient client.Client + rbacUsecase RBACUsecase } // NewProjectUsecase new project usecase -func NewProjectUsecase(ds datastore.DataStore) ProjectUsecase { +func NewProjectUsecase(ds datastore.DataStore, rbacUsecase RBACUsecase) ProjectUsecase { k8sClient, err := clients.GetKubeClient() if err != nil { log.Logger.Fatalf("get k8sClient failure: %s", err.Error()) } - p := &projectUsecaseImpl{ds: ds, k8sClient: k8sClient} - p.initDefaultProjectEnvTarget(model.DefaultInitNamespace) + p := &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} return p } +// Init init default data +func (p *projectUsecaseImpl) Init(ctx context.Context) error { + return p.InitDefaultProjectEnvTarget(ctx, model.DefaultInitNamespace) +} + // initDefaultProjectEnvTarget will initialize a default project with a default env that contain a default target // the default env and default target both using the `default` namespace in control plane cluster -func (p *projectUsecaseImpl) initDefaultProjectEnvTarget(defaultNamespace string) { - - ctx := context.Background() - projResp, err := listProjects(ctx, p.ds, 0, 0) +func (p *projectUsecaseImpl) InitDefaultProjectEnvTarget(ctx context.Context, defaultNamespace string) error { + var project = model.Project{} + entities, err := p.ds.List(ctx, &project, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{ + IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "owner", + }, + }, + }}) if err != nil { - log.Logger.Errorf("initialize project failed %v", err) - return + return fmt.Errorf("initialize project failed %w", err) } - if len(projResp.Projects) > 0 { - return + if len(entities) > 0 { + for _, project := range entities { + pro := project.(*model.Project) + var init = pro.Owner == "" + pro.Owner = model.DefaultAdminUserName + if err := p.ds.Put(ctx, pro); err != nil { + return err + } + // owner is empty, it is old data + if init { + if err := p.rbacUsecase.InitDefaultRoleAndUsersForProject(ctx, pro); err != nil { + return fmt.Errorf("init default role and users for project %s failure %w", pro.Name, err) + } + } + } + return nil + } + + count, _ := p.ds.Count(ctx, &project, nil) + if count > 0 { + return nil } log.Logger.Info("no default project found, adding a default project with default env and target") if err := createTargetNamespace(ctx, p.k8sClient, multicluster.ClusterLocalName, defaultNamespace, model.DefaultInitName); err != nil { - log.Logger.Errorf("initialize default target namespace failed %v", err) - return + return fmt.Errorf("initialize default target namespace failed %w", err) } // initialize default target first err = createTarget(ctx, p.ds, &model.Target{ @@ -85,8 +122,7 @@ func (p *projectUsecaseImpl) initDefaultProjectEnvTarget(defaultNamespace string }) // for idempotence, ignore default target already exist error if err != nil && errors.Is(err, bcode.ErrTargetExist) { - log.Logger.Errorf("initialize default target failed %v", err) - return + return fmt.Errorf("initialize default target failed %w", err) } // initialize default target first @@ -100,20 +136,20 @@ func (p *projectUsecaseImpl) initDefaultProjectEnvTarget(defaultNamespace string }) // for idempotence, ignore default env already exist error if err != nil && errors.Is(err, bcode.ErrEnvAlreadyExists) { - log.Logger.Errorf("initialize default environment failed %v", err) - return + + return fmt.Errorf("initialize default environment failed %w", err) } _, err = p.CreateProject(ctx, apisv1.CreateProjectRequest{ Name: model.DefaultInitName, Alias: "Default", Description: model.DefaultProjectDescription, + Owner: model.DefaultAdminUserName, }) if err != nil { - log.Logger.Errorf("initialize project failed %v", err) - return + return fmt.Errorf("initialize project failed %w", err) } - + return nil } // GetProject get project @@ -128,24 +164,73 @@ func (p *projectUsecaseImpl) GetProject(ctx context.Context, projectName string) return project, nil } +func (p *projectUsecaseImpl) DetailProject(ctx context.Context, projectName string) (*apisv1.ProjectBase, error) { + project, err := p.GetProject(ctx, projectName) + if err != nil { + return nil, err + } + var user = &model.User{Name: project.Owner} + if project.Owner != "" { + if err := p.ds.Get(ctx, user); err != nil { + log.Logger.Warnf("get project owner %s info failure %s", project.Owner, err.Error()) + } + } + return ConvertProjectModel2Base(project, user), nil +} + func listProjects(ctx context.Context, ds datastore.DataStore, page, pageSize int) (*apisv1.ListProjectResponse, error) { var project = model.Project{} - entitys, err := ds.List(ctx, &project, &datastore.ListOptions{Page: page, PageSize: pageSize, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) + entities, err := ds.List(ctx, &project, &datastore.ListOptions{Page: page, PageSize: pageSize, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) if err != nil { return nil, err } var projects []*apisv1.ProjectBase - for _, entity := range entitys { + for _, entity := range entities { project := entity.(*model.Project) - projects = append(projects, convertProjectModel2Base(project)) + var user = &model.User{Name: project.Owner} + if project.Owner != "" { + if err := ds.Get(ctx, user); err != nil { + log.Logger.Warnf("get project owner %s info failure %s", project.Owner, err.Error()) + } + } + projects = append(projects, ConvertProjectModel2Base(project, user)) } - total, err := ds.Count(ctx, &model.Project{}, nil) + total, err := ds.Count(ctx, &project, nil) if err != nil { return nil, err } return &apisv1.ListProjectResponse{Projects: projects, Total: total}, nil } +func (p *projectUsecaseImpl) ListUserProjects(ctx context.Context, userName string) ([]*apisv1.ProjectBase, error) { + var projectUser = model.ProjectUser{ + Username: userName, + } + entities, err := p.ds.List(ctx, &projectUser, nil) + if err != nil { + return nil, err + } + var projectNames []string + for _, entity := range entities { + projectNames = append(projectNames, entity.(*model.ProjectUser).ProjectName) + } + if len(projectNames) == 0 { + return []*apisv1.ProjectBase{}, nil + } + projectEntities, err := p.ds.List(ctx, &model.Project{}, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{In: []datastore.InQueryOption{{ + Key: "name", + Values: projectNames, + }}}}) + if err != nil { + return nil, err + } + var projectBases []*apisv1.ProjectBase + for _, entity := range projectEntities { + projectBases = append(projectBases, ConvertProjectModel2Base(entity.(*model.Project), nil)) + } + return projectBases, nil +} + // ListProjects list projects func (p *projectUsecaseImpl) ListProjects(ctx context.Context, page, pageSize int) (*apisv1.ListProjectResponse, error) { return listProjects(ctx, p.ds, page, pageSize) @@ -153,10 +238,58 @@ func (p *projectUsecaseImpl) ListProjects(ctx context.Context, page, pageSize in // DeleteProject delete a project func (p *projectUsecaseImpl) DeleteProject(ctx context.Context, name string) error { + _, err := p.GetProject(ctx, name) + if err != nil { + return err + } - // TODO(@wonderflow): it's not supported for delete a project now, just used in test - // we should prevent delete a project that contain any application/env inside. + count, err := p.ds.Count(ctx, &model.Application{Project: name}, nil) + if err != nil { + return err + } + if count > 0 { + return bcode.ErrProjectDenyDeleteByApplication + } + count, err = p.ds.Count(ctx, &model.Target{Project: name}, nil) + if err != nil { + return err + } + if count > 0 { + return bcode.ErrProjectDenyDeleteByTarget + } + + count, err = p.ds.Count(ctx, &model.Env{Project: name}, nil) + if err != nil { + return err + } + if count > 0 { + return bcode.ErrProjectDenyDeleteByEnvironment + } + + users, _ := p.ListProjectUser(ctx, name, 0, 0) + for _, user := range users.Users { + err := p.DeleteProjectUser(ctx, name, user.UserName) + if err != nil { + return err + } + } + + roles, _ := p.rbacUsecase.ListRole(ctx, name, 0, 0) + for _, role := range roles.Roles { + err := p.rbacUsecase.DeleteRole(ctx, name, role.Name) + if err != nil { + return err + } + } + + permissions, _ := p.rbacUsecase.ListPermissions(ctx, name) + for _, perm := range permissions { + err := p.rbacUsecase.DeletePermission(ctx, name, perm.Name) + if err != nil { + return err + } + } return p.ds.Delete(ctx, &model.Project{Name: name}) } @@ -171,32 +304,191 @@ func (p *projectUsecaseImpl) CreateProject(ctx context.Context, req apisv1.Creat if exist { return nil, bcode.ErrProjectIsExist } + owner := req.Owner + if owner == "" { + loginUserName, ok := ctx.Value(&apisv1.CtxKeyUser).(string) + if ok { + owner = loginUserName + } + } + var user = &model.User{Name: owner} + if owner != "" { + if err := p.ds.Get(ctx, user); err != nil { + return nil, bcode.ErrProjectOwnerIsNotExist + } + } newProject := &model.Project{ Name: req.Name, Description: req.Description, Alias: req.Alias, + Owner: owner, } if err := p.ds.Add(ctx, newProject); err != nil { return nil, err } - return &apisv1.ProjectBase{ - Name: newProject.Name, - Alias: newProject.Alias, - Description: newProject.Description, - CreateTime: newProject.CreateTime, - UpdateTime: newProject.UpdateTime, - }, nil + if err := p.rbacUsecase.InitDefaultRoleAndUsersForProject(ctx, newProject); err != nil { + log.Logger.Errorf("init default role and users for project failure %s", err.Error()) + } + + return ConvertProjectModel2Base(newProject, user), nil } -func convertProjectModel2Base(project *model.Project) *apisv1.ProjectBase { - return &apisv1.ProjectBase{ +// UpdateProject update project +func (p *projectUsecaseImpl) UpdateProject(ctx context.Context, projectName string, req apisv1.UpdateProjectRequest) (*apisv1.ProjectBase, error) { + project, err := p.GetProject(ctx, projectName) + if err != nil { + return nil, err + } + project.Alias = req.Alias + project.Description = req.Description + var user = &model.User{Name: req.Owner} + if req.Owner != "" { + if err := p.ds.Get(ctx, user); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrProjectOwnerIsNotExist + } + return nil, err + } + project.Owner = req.Owner + } + err = p.ds.Put(ctx, project) + if err != nil { + return nil, err + } + return ConvertProjectModel2Base(project, user), nil +} + +func (p *projectUsecaseImpl) ListProjectUser(ctx context.Context, projectName string, page, pageSize int) (*apisv1.ListProjectUsersResponse, error) { + var projectUser = model.ProjectUser{ + ProjectName: projectName, + } + entities, err := p.ds.List(ctx, &projectUser, &datastore.ListOptions{Page: page, PageSize: pageSize, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) + if err != nil { + return nil, err + } + var res apisv1.ListProjectUsersResponse + for _, entity := range entities { + res.Users = append(res.Users, ConvertProjectUserModel2Base(entity.(*model.ProjectUser))) + } + count, err := p.ds.Count(ctx, &projectUser, nil) + if err != nil { + return nil, err + } + res.Total = count + return &res, nil +} + +func (p *projectUsecaseImpl) AddProjectUser(ctx context.Context, projectName string, req apisv1.AddProjectUserRequest) (*apisv1.ProjectUserBase, error) { + project, err := p.GetProject(ctx, projectName) + if err != nil { + return nil, err + } + // check user roles + for _, role := range req.UserRoles { + var projectUser = model.Role{ + Name: role, + Project: projectName, + } + if err := p.ds.Get(ctx, &projectUser); err != nil { + return nil, bcode.ErrProjectRoleCheckFailure + } + if projectUser.Project != "" && projectUser.Project != projectName { + return nil, bcode.ErrProjectRoleCheckFailure + } + } + var projectUser = model.ProjectUser{ + Username: req.UserName, + ProjectName: project.Name, + UserRoles: req.UserRoles, + } + if err := p.ds.Add(ctx, &projectUser); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrProjectUserExist + } + return nil, err + } + return ConvertProjectUserModel2Base(&projectUser), nil +} + +func (p *projectUsecaseImpl) DeleteProjectUser(ctx context.Context, projectName string, userName string) error { + project, err := p.GetProject(ctx, projectName) + if err != nil { + return err + } + var projectUser = model.ProjectUser{ + Username: userName, + ProjectName: project.Name, + } + if err := p.ds.Delete(ctx, &projectUser); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return bcode.ErrProjectUserExist + } + return err + } + return nil +} + +func (p *projectUsecaseImpl) UpdateProjectUser(ctx context.Context, projectName string, userName string, req apisv1.UpdateProjectUserRequest) (*apisv1.ProjectUserBase, error) { + project, err := p.GetProject(ctx, projectName) + if err != nil { + return nil, err + } + // check user roles + for _, role := range req.UserRoles { + var projectUser = model.Role{ + Name: role, + Project: projectName, + } + if err := p.ds.Get(ctx, &projectUser); err != nil { + return nil, bcode.ErrProjectRoleCheckFailure + } + if projectUser.Project != "" && projectUser.Project != projectName { + return nil, bcode.ErrProjectRoleCheckFailure + } + } + var projectUser = model.ProjectUser{ + Username: userName, + ProjectName: project.Name, + } + if err := p.ds.Get(ctx, &projectUser); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrProjectUserExist + } + return nil, err + } + projectUser.UserRoles = req.UserRoles + if err := p.ds.Put(ctx, &projectUser); err != nil { + return nil, err + } + return ConvertProjectUserModel2Base(&projectUser), nil +} + +// ConvertProjectModel2Base convert project model to base struct +func ConvertProjectModel2Base(project *model.Project, owner *model.User) *apisv1.ProjectBase { + base := &apisv1.ProjectBase{ Name: project.Name, Description: project.Description, Alias: project.Alias, CreateTime: project.CreateTime, UpdateTime: project.UpdateTime, + Owner: apisv1.NameAlias{Name: project.Owner}, } + if owner != nil && owner.Name == project.Owner { + base.Owner = apisv1.NameAlias{Name: owner.Name, Alias: owner.Alias} + } + return base +} + +// ConvertProjectUserModel2Base convert project user model to base struct +func ConvertProjectUserModel2Base(user *model.ProjectUser) *apisv1.ProjectUserBase { + base := &apisv1.ProjectUserBase{ + UserName: user.Username, + UserRoles: user.UserRoles, + CreateTime: user.CreateTime, + UpdateTime: user.UpdateTime, + } + return base } diff --git a/pkg/apiserver/rest/usecase/project_test.go b/pkg/apiserver/rest/usecase/project_test.go index 065eebf4f..1d98150c3 100644 --- a/pkg/apiserver/rest/usecase/project_test.go +++ b/pkg/apiserver/rest/usecase/project_test.go @@ -26,9 +26,11 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" @@ -38,6 +40,7 @@ var _ = Describe("Test project usecase functions", func() { var ( projectUsecase *projectUsecaseImpl envImpl *envUsecaseImpl + userUsecase *userUsecaseImpl targetImpl *targetUsecaseImpl defaultNamespace = "project-default-ns1-test" ) @@ -45,11 +48,13 @@ var _ = Describe("Test project usecase functions", func() { ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "target-test-kubevela"}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) + userUsecase = &userUsecaseImpl{ds: ds, k8sClient: k8sClient} var ns = corev1.Namespace{} ns.Name = defaultNamespace err = k8sClient.Create(context.TODO(), &ns) Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - projectUsecase = &projectUsecaseImpl{k8sClient: k8sClient, ds: ds} + + projectUsecase = &projectUsecaseImpl{k8sClient: k8sClient, ds: ds, rbacUsecase: &rbacUsecaseImpl{ds: ds}} pp, err := projectUsecase.ListProjects(context.TODO(), 0, 0) Expect(err).Should(BeNil()) // reset all projects @@ -57,24 +62,35 @@ var _ = Describe("Test project usecase functions", func() { _ = projectUsecase.DeleteProject(context.TODO(), p.Name) } - envImpl = &envUsecaseImpl{kubeClient: k8sClient, ds: ds} - envs, err := envImpl.ListEnvs(context.TODO(), 0, 0, apisv1.ListEnvOptions{}) + envImpl = &envUsecaseImpl{kubeClient: k8sClient, ds: ds, projectUsecase: projectUsecase} + ctx := context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "admin") + envs, err := envImpl.ListEnvs(ctx, 0, 0, apisv1.ListEnvOptions{}) Expect(err).Should(BeNil()) // reset all projects for _, e := range envs.Envs { _ = envImpl.DeleteEnv(context.TODO(), e.Name) } targetImpl = &targetUsecaseImpl{k8sClient: k8sClient, ds: ds} - targs, err := targetImpl.ListTargets(context.TODO(), 0, 0) + targets, err := targetImpl.ListTargets(context.TODO(), 0, 0, "") Expect(err).Should(BeNil()) // reset all projects - for _, e := range targs.Targets { - _ = envImpl.DeleteEnv(context.TODO(), e.Name) + for _, t := range targets.Targets { + _ = targetImpl.DeleteTarget(context.TODO(), t.Name) } }) It("Test project initialize function", func() { - projectUsecase.initDefaultProjectEnvTarget(defaultNamespace) + // init admin user + var ns = corev1.Namespace{} + ns.Name = velatypes.DefaultKubeVelaNS + err := k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + err = userUsecase.Init(context.TODO()) + Expect(err).Should(BeNil()) + + // init default project + err = projectUsecase.InitDefaultProjectEnvTarget(context.WithValue(context.TODO(), &apisv1.CtxKeyUser, model.DefaultAdminUserName), defaultNamespace) + Expect(err).Should(BeNil()) By("test env created") var namespace corev1.Namespace Eventually(func() error { @@ -134,4 +150,97 @@ var _ = Describe("Test project usecase functions", func() { projectUsecase.DeleteProject(context.TODO(), "test-project") }) + It("Test Update project function", func() { + req := apisv1.CreateProjectRequest{ + Name: "test-project", + Description: "this is a project description", + } + _, err := projectUsecase.CreateProject(context.TODO(), req) + Expect(err).Should(BeNil()) + + base, err := projectUsecase.UpdateProject(context.TODO(), "test-project", apisv1.UpdateProjectRequest{ + Alias: "Change alias", + Description: "Change description", + Owner: "admin", + }) + Expect(err).Should(BeNil()) + Expect(base.Alias).Should(BeEquivalentTo("Change alias")) + Expect(base.Description).Should(BeEquivalentTo("Change description")) + Expect(base.Owner.Alias).Should(BeEquivalentTo("Administrator")) + + _, err = projectUsecase.UpdateProject(context.TODO(), "test-project", apisv1.UpdateProjectRequest{ + Alias: "Change alias", + Description: "Change description", + Owner: "admin-error", + }) + Expect(err).Should(BeEquivalentTo(bcode.ErrProjectOwnerIsNotExist)) + err = projectUsecase.DeleteProject(context.TODO(), "test-project") + Expect(err).Should(BeNil()) + }) + + It("Test Create project user function", func() { + req := apisv1.CreateProjectRequest{ + Name: "test-project", + Description: "this is a project description", + } + _, err := projectUsecase.CreateProject(context.TODO(), req) + Expect(err).Should(BeNil()) + + _, err = projectUsecase.AddProjectUser(context.TODO(), "test-project", apisv1.AddProjectUserRequest{ + UserName: "admin", + UserRoles: []string{"project-admin"}, + }) + Expect(err).Should(BeNil()) + }) + + It("Test Update project user function", func() { + req := apisv1.CreateProjectRequest{ + Name: "test-project", + Description: "this is a project description", + } + _, err := projectUsecase.CreateProject(context.TODO(), req) + Expect(err).Should(BeNil()) + + _, err = projectUsecase.AddProjectUser(context.TODO(), "test-project", apisv1.AddProjectUserRequest{ + UserName: "admin", + UserRoles: []string{"project-admin"}, + }) + Expect(err).Should(BeNil()) + + _, err = projectUsecase.UpdateProjectUser(context.TODO(), "test-project", "admin", apisv1.UpdateProjectUserRequest{ + UserRoles: []string{"project-admin", "app-developer"}, + }) + Expect(err).Should(BeNil()) + + _, err = projectUsecase.UpdateProjectUser(context.TODO(), "test-project", "admin", apisv1.UpdateProjectUserRequest{ + UserRoles: []string{"project-admin", "app-developer", "xxx"}, + }) + Expect(err).Should(BeEquivalentTo(bcode.ErrProjectRoleCheckFailure)) + }) + + It("Test delete project user and delete project function", func() { + req := apisv1.CreateProjectRequest{ + Name: "test-project", + Description: "this is a project description", + } + _, err := projectUsecase.CreateProject(context.TODO(), req) + Expect(err).Should(BeNil()) + + _, err = projectUsecase.AddProjectUser(context.TODO(), "test-project", apisv1.AddProjectUserRequest{ + UserName: "admin", + UserRoles: []string{"project-admin"}, + }) + Expect(err).Should(BeNil()) + + err = projectUsecase.DeleteProjectUser(context.TODO(), "test-project", "admin") + Expect(err).Should(BeNil()) + err = projectUsecase.DeleteProject(context.TODO(), "test-project") + Expect(err).Should(BeNil()) + perms, err := projectUsecase.rbacUsecase.ListPermissions(context.TODO(), "test-project") + Expect(err).Should(BeNil()) + Expect(len(perms)).Should(BeEquivalentTo(0)) + roles, err := projectUsecase.rbacUsecase.ListRole(context.TODO(), "test-project", 0, 0) + Expect(err).Should(BeNil()) + Expect(roles.Total).Should(BeEquivalentTo(0)) + }) }) diff --git a/pkg/apiserver/rest/usecase/rbac.go b/pkg/apiserver/rest/usecase/rbac.go new file mode 100644 index 000000000..639cf907c --- /dev/null +++ b/pkg/apiserver/rest/usecase/rbac.go @@ -0,0 +1,913 @@ +/* +Copyright 2022 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package usecase + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/emicklei/go-restful/v3" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/utils" +) + +// resourceActions all register resources and actions +var resourceActions map[string][]string +var lock sync.Mutex +var reg = regexp.MustCompile(`(?U)\{.*\}`) + +var defaultProjectPermissionTemplate = []*model.PermissionTemplate{ + { + Name: "project-read", + Alias: "Project Read", + Resources: []string{"project:{projectName}"}, + Actions: []string{"detail"}, + Effect: "Allow", + Scope: "project", + }, + { + Name: "app-management", + Alias: "App Management", + Resources: []string{"project:{projectName}/application:*/*", "definition:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "project", + }, + { + Name: "env-management", + Alias: "Environment Management", + Resources: []string{"project:{projectName}/environment:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "project", + }, + { + Name: "role-management", + Alias: "Role Management", + Resources: []string{"project:{projectName}/role:*", "project:{projectName}/projectUser:*", "project:{projectName}/permission:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "project", + }, +} + +var defaultPlatformPermission = []*model.PermissionTemplate{ + { + Name: "cluster-manage", + Alias: "Cluster Management", + Resources: []string{"cluster:*/*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, + { + Name: "project-management", + Alias: "Project Management", + Resources: []string{"project:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, + { + Name: "addon-management", + Alias: "Addon Management", + Resources: []string{"addon:*", "addonRegistry:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, + { + Name: "target-manage", + Alias: "Target Management", + Resources: []string{"target:*", "cluster:*/namespace:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, + { + Name: "user-manage", + Alias: "User Management", + Resources: []string{"user:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, + { + Name: "role-manage", + Alias: "Platform Role Management", + Resources: []string{"role:*", "permission:*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, + { + Name: "admin", + Alias: "Admin", + Resources: []string{"*"}, + Actions: []string{"*"}, + Effect: "Allow", + Scope: "platform", + }, +} + +// ResourceMaps all resources definition for RBAC +var ResourceMaps = map[string]resourceMetadata{ + "project": { + subResources: map[string]resourceMetadata{ + "application": { + pathName: "appName", + subResources: map[string]resourceMetadata{ + "component": { + subResources: map[string]resourceMetadata{ + "trait": { + pathName: "traitType", + }, + }, + pathName: "compName", + }, + "workflow": { + subResources: map[string]resourceMetadata{ + "record": { + pathName: "record", + }, + }, + pathName: "workflowName", + }, + "policy": { + pathName: "policyName", + }, + "revision": { + pathName: "revision", + }, + "envBinding": { + pathName: "envName", + }, + "trigger": {}, + }, + }, + "environment": { + pathName: "envName", + }, + "workflow": { + pathName: "workflowName", + }, + "role": { + pathName: "roleName", + }, + "permission": {}, + "projectUser": { + pathName: "userName", + }, + "applicationTemplate": {}, + }, + pathName: "projectName", + }, + "cluster": { + pathName: "clusterName", + subResources: map[string]resourceMetadata{ + "namespace": {}, + }, + }, + "addon": { + pathName: "addonName", + }, + "addonRegistry": { + pathName: "addonRegName", + }, + "target": { + pathName: "targetName", + }, + "user": { + pathName: "userName", + }, + "role": {}, + "permission": {}, + "systemSetting": {}, + "definition": {}, +} + +var existResourcePaths = convert(ResourceMaps) + +type resourceMetadata struct { + subResources map[string]resourceMetadata + pathName string +} + +func checkResourcePath(resource string) (string, error) { + if sub, exist := ResourceMaps[resource]; exist { + if sub.pathName != "" { + return fmt.Sprintf("%s:{%s}", resource, sub.pathName), nil + } + return fmt.Sprintf("%s:*", resource), nil + } + path := "" + exist := 0 + lastResourceName := resource[strings.LastIndex(resource, "/")+1:] + for key, erp := range existResourcePaths { + allMatchIndex := strings.Index(key, fmt.Sprintf("/%s/", resource)) + index := strings.Index(erp, fmt.Sprintf("/%s:", lastResourceName)) + if index > -1 && allMatchIndex > -1 { + pre := erp[:index+len(lastResourceName)+2] + next := strings.Replace(erp, pre, "", 1) + nameIndex := strings.Index(next, "/") + if nameIndex > -1 { + pre += next[:nameIndex] + } + if pre != path { + exist++ + } + path = pre + } + } + path = strings.TrimSuffix(path, "/") + path = strings.TrimPrefix(path, "/") + if exist == 1 { + return path, nil + } + if exist > 1 { + return path, fmt.Errorf("the resource name %s is not unique", resource) + } + return path, fmt.Errorf("there is no resource %s", resource) +} + +func convert(sources map[string]resourceMetadata) map[string]string { + list := make(map[string]string) + for k, v := range sources { + if len(v.subResources) > 0 { + for sub, subWithPathName := range convert(v.subResources) { + if subWithPathName != "" { + withPathname := fmt.Sprintf("/%s:*%s", k, subWithPathName) + if v.pathName != "" { + withPathname = fmt.Sprintf("/%s:{%s}%s", k, v.pathName, subWithPathName) + } + list[fmt.Sprintf("/%s%s", k, sub)] = withPathname + } + } + } + withPathname := fmt.Sprintf("/%s:*/", k) + if v.pathName != "" { + withPathname = fmt.Sprintf("/%s:{%s}/", k, v.pathName) + } + list[fmt.Sprintf("/%s/", k)] = withPathname + } + return list +} + +// registerResourceAction register resource actions +func registerResourceAction(resource string, actions ...string) { + lock.Lock() + defer lock.Unlock() + if resourceActions == nil { + resourceActions = make(map[string][]string) + } + path, err := checkResourcePath(resource) + if err != nil { + panic(fmt.Sprintf("resource %s is not exist", resource)) + } + resource = path + if _, exist := resourceActions[resource]; exist { + for _, action := range actions { + if !utils.StringsContain(resourceActions[resource], action) { + resourceActions[resource] = append(resourceActions[resource], action) + } + } + } else { + resourceActions[resource] = actions + } +} + +type rbacUsecaseImpl struct { + ds datastore.DataStore +} + +// RBACUsecase implement RBAC-related business logic. +type RBACUsecase interface { + CheckPerm(resource string, actions ...string) func(req *restful.Request, res *restful.Response, chain *restful.FilterChain) + GetUserPermissions(ctx context.Context, user *model.User, projectName string, withPlatform bool) ([]*model.Permission, error) + CreateRole(ctx context.Context, projectName string, req apisv1.CreateRoleRequest) (*apisv1.RoleBase, error) + DeleteRole(ctx context.Context, projectName, roleName string) error + UpdateRole(ctx context.Context, projectName, roleName string, req apisv1.UpdateRoleRequest) (*apisv1.RoleBase, error) + ListRole(ctx context.Context, projectName string, page, pageSize int) (*apisv1.ListRolesResponse, error) + ListPermissionTemplate(ctx context.Context, projectName string) ([]apisv1.PermissionTemplateBase, error) + ListPermissions(ctx context.Context, projectName string) ([]apisv1.PermissionBase, error) + DeletePermission(ctx context.Context, projectName, permName string) error + InitDefaultRoleAndUsersForProject(ctx context.Context, project *model.Project) error + Init(ctx context.Context) error +} + +// NewRBACUsecase is the usecase service of RBAC +func NewRBACUsecase(ds datastore.DataStore) RBACUsecase { + rbacUsecase := &rbacUsecaseImpl{ + ds: ds, + } + return rbacUsecase +} + +func (p *rbacUsecaseImpl) Init(ctx context.Context) error { + count, _ := p.ds.Count(ctx, &model.Permission{}, &datastore.FilterOptions{ + IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }, + }) + if count > 0 { + return nil + } + var batchData []datastore.Entity + for _, policy := range defaultPlatformPermission { + batchData = append(batchData, &model.Permission{ + Name: policy.Name, + Alias: policy.Alias, + Resources: policy.Resources, + Actions: policy.Actions, + Effect: policy.Effect, + }) + } + batchData = append(batchData, &model.Role{ + Name: "admin", + Alias: "Admin", + Permissions: []string{"admin"}, + }) + if err := p.ds.BatchAdd(context.Background(), batchData); err != nil { + return fmt.Errorf("init the platform perm policies failure %w", err) + } + return nil +} + +// GetUserPermissions get user permission policies, if projectName is empty, will only get the platform permission policies +func (p *rbacUsecaseImpl) GetUserPermissions(ctx context.Context, user *model.User, projectName string, withPlatform bool) ([]*model.Permission, error) { + var permissionNames []string + var perms []*model.Permission + if withPlatform && len(user.UserRoles) > 0 { + entities, err := p.ds.List(ctx, &model.Role{}, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{ + In: []datastore.InQueryOption{ + { + Key: "name", + Values: user.UserRoles, + }, + }, + IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }, + }}) + if err != nil { + return nil, err + } + for _, entity := range entities { + permissionNames = append(permissionNames, entity.(*model.Role).Permissions...) + } + perms, err = p.listPermPolices(ctx, "", permissionNames) + if err != nil { + return nil, err + } + } + if projectName != "" { + var projectUser = model.ProjectUser{ + ProjectName: projectName, + Username: user.Name, + } + var roles []string + if err := p.ds.Get(ctx, &projectUser); err == nil { + roles = append(roles, projectUser.UserRoles...) + } + if len(roles) > 0 { + entities, err := p.ds.List(ctx, &model.Role{Project: projectName}, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{In: []datastore.InQueryOption{ + { + Key: "name", + Values: roles, + }, + }}}) + if err != nil { + return nil, err + } + for _, entity := range entities { + permissionNames = append(permissionNames, entity.(*model.Role).Permissions...) + } + projectPerms, err := p.listPermPolices(ctx, projectName, permissionNames) + if err != nil { + return nil, err + } + perms = append(perms, projectPerms...) + } + } + return perms, nil +} + +func (p *rbacUsecaseImpl) UpdatePermission(ctx context.Context, projetName string, permissionName string, req *apisv1.UpdatePermissionRequest) (*apisv1.PermissionBase, error) { + perm := &model.Permission{ + Project: projetName, + Name: permissionName, + } + err := p.ds.Get(ctx, perm) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrPermissionNotExist + } + } + //TODO: check req validate + perm.Actions = req.Actions + perm.Alias = req.Alias + perm.Resources = req.Resources + perm.Effect = req.Effect + if err := p.ds.Put(ctx, perm); err != nil { + return nil, err + } + return &apisv1.PermissionBase{ + Name: perm.Name, + Alias: perm.Alias, + Resources: perm.Resources, + Actions: perm.Actions, + Effect: perm.Effect, + CreateTime: perm.CreateTime, + UpdateTime: perm.UpdateTime, + }, nil +} + +func (p *rbacUsecaseImpl) listPermPolices(ctx context.Context, projectName string, permissionNames []string) ([]*model.Permission, error) { + if len(permissionNames) == 0 { + return []*model.Permission{}, nil + } + filter := datastore.FilterOptions{In: []datastore.InQueryOption{ + { + Key: "name", + Values: permissionNames, + }, + }} + if projectName == "" { + filter.IsNotExist = append(filter.IsNotExist, datastore.IsNotExistQueryOption{ + Key: "project", + }) + } + permEntities, err := p.ds.List(ctx, &model.Permission{Project: projectName}, &datastore.ListOptions{FilterOptions: filter}) + if err != nil { + return nil, err + } + var perms []*model.Permission + for _, entity := range permEntities { + perms = append(perms, entity.(*model.Permission)) + } + return perms, nil +} + +func (p *rbacUsecaseImpl) CheckPerm(resource string, actions ...string) func(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + registerResourceAction(resource, actions...) + f := func(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + // get login user info + userName, ok := req.Request.Context().Value(&apisv1.CtxKeyUser).(string) + if !ok { + bcode.ReturnError(req, res, bcode.ErrUnauthorized) + return + } + user := &model.User{Name: userName} + if err := p.ds.Get(req.Request.Context(), user); err != nil { + bcode.ReturnError(req, res, bcode.ErrUnauthorized) + return + } + path, err := checkResourcePath(resource) + if err != nil { + log.Logger.Errorf("check resource path failure %s", err.Error()) + bcode.ReturnError(req, res, bcode.ErrForbidden) + return + } + + // multiple method for get the project name. + getProjectName := func() string { + if value := req.PathParameter("projectName"); value != "" { + return value + } + if value := req.QueryParameter("project"); value != "" { + return value + } + if value := req.QueryParameter("projectName"); value != "" { + return value + } + if appName := req.PathParameter(ResourceMaps["project"].subResources["application"].pathName); appName != "" { + app := &model.Application{Name: appName} + if err := p.ds.Get(req.Request.Context(), app); err == nil { + return app.Project + } + } + if envName := req.PathParameter(ResourceMaps["project"].subResources["environment"].pathName); envName != "" { + env := &model.Env{Name: envName} + if err := p.ds.Get(req.Request.Context(), env); err == nil { + return env.Project + } + } + return "" + } + + ra := &RequestResourceAction{} + ra.SetResourceWithName(path, func(name string) string { + if name == ResourceMaps["project"].pathName { + return getProjectName() + } + return req.PathParameter(name) + }) + + // get user's perm list. + projectName := getProjectName() + permissions, err := p.GetUserPermissions(req.Request.Context(), user, projectName, true) + if err != nil { + log.Logger.Errorf("get user's perm policies failure %s, user is %s", err.Error(), user.Name) + bcode.ReturnError(req, res, bcode.ErrForbidden) + return + } + if !ra.Match(permissions) { + bcode.ReturnError(req, res, bcode.ErrForbidden) + return + } + chain.ProcessFilter(req, res) + } + return f +} + +func (p *rbacUsecaseImpl) CreateRole(ctx context.Context, projectName string, req apisv1.CreateRoleRequest) (*apisv1.RoleBase, error) { + if projectName != "" { + var project = model.Project{ + Name: projectName, + } + if err := p.ds.Get(ctx, &project); err != nil { + return nil, bcode.ErrProjectIsNotExist + } + } + if len(req.Permissions) == 0 { + return nil, bcode.ErrRolePermissionCheckFailure + } + policies, err := p.listPermPolices(ctx, projectName, req.Permissions) + if err != nil || len(policies) != len(req.Permissions) { + return nil, bcode.ErrRolePermissionCheckFailure + } + var role = model.Role{ + Name: req.Name, + Alias: req.Alias, + Project: projectName, + Permissions: req.Permissions, + } + if err := p.ds.Add(ctx, &role); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrRoleIsExist + } + return nil, err + } + return ConvertRole2Model(&role, policies), nil +} + +func (p *rbacUsecaseImpl) DeleteRole(ctx context.Context, projectName, roleName string) error { + var role = model.Role{ + Name: roleName, + Project: projectName, + } + if err := p.ds.Delete(ctx, &role); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrRoleIsNotExist + } + return err + } + return nil +} + +func (p *rbacUsecaseImpl) DeletePermission(ctx context.Context, projectName, permName string) error { + var perm = model.Permission{ + Name: permName, + Project: projectName, + } + if err := p.ds.Delete(ctx, &perm); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrRoleIsNotExist + } + return err + } + return nil +} + +func (p *rbacUsecaseImpl) UpdateRole(ctx context.Context, projectName, roleName string, req apisv1.UpdateRoleRequest) (*apisv1.RoleBase, error) { + if projectName != "" { + var project = model.Project{ + Name: projectName, + } + if err := p.ds.Get(ctx, &project); err != nil { + return nil, bcode.ErrProjectIsNotExist + } + } + if len(req.Permissions) == 0 { + return nil, bcode.ErrRolePermissionCheckFailure + } + policies, err := p.listPermPolices(ctx, projectName, req.Permissions) + if err != nil || len(policies) != len(req.Permissions) { + return nil, bcode.ErrRolePermissionCheckFailure + } + var role = model.Role{ + Name: roleName, + Project: projectName, + } + if err := p.ds.Get(ctx, &role); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrRoleIsNotExist + } + return nil, err + } + role.Alias = req.Alias + role.Permissions = req.Permissions + if err := p.ds.Put(ctx, &role); err != nil { + return nil, err + } + return ConvertRole2Model(&role, policies), nil +} + +func (p *rbacUsecaseImpl) ListRole(ctx context.Context, projectName string, page, pageSize int) (*apisv1.ListRolesResponse, error) { + var role = model.Role{ + Project: projectName, + } + var filter datastore.FilterOptions + if projectName == "" { + filter.IsNotExist = append(filter.IsNotExist, datastore.IsNotExistQueryOption{ + Key: "project", + }) + } + entities, err := p.ds.List(ctx, &role, &datastore.ListOptions{FilterOptions: filter, Page: page, PageSize: pageSize, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) + if err != nil { + return nil, err + } + var policySet = make(map[string]string) + for _, entity := range entities { + for _, p := range entity.(*model.Role).Permissions { + policySet[p] = p + } + } + + policies, err := p.listPermPolices(ctx, projectName, utils.MapKey2Array(policySet)) + if err != nil { + log.Logger.Errorf("list perm policies failure %s", err.Error()) + } + var policyMap = make(map[string]*model.Permission) + for i, policy := range policies { + policyMap[policy.Name] = policies[i] + } + var res apisv1.ListRolesResponse + for _, entity := range entities { + role := entity.(*model.Role) + var rolePolicies []*model.Permission + for _, perm := range role.Permissions { + rolePolicies = append(rolePolicies, policyMap[perm]) + } + res.Roles = append(res.Roles, ConvertRole2Model(entity.(*model.Role), rolePolicies)) + } + count, err := p.ds.Count(ctx, &role, &filter) + if err != nil { + return nil, err + } + res.Total = count + return &res, nil +} + +// ListPermissionTemplate TODO: +func (p *rbacUsecaseImpl) ListPermissionTemplate(ctx context.Context, projectName string) ([]apisv1.PermissionTemplateBase, error) { + return nil, nil +} + +func (p *rbacUsecaseImpl) ListPermissions(ctx context.Context, projectName string) ([]apisv1.PermissionBase, error) { + var filter datastore.FilterOptions + if projectName == "" { + filter.IsNotExist = append(filter.IsNotExist, datastore.IsNotExistQueryOption{ + Key: "project", + }) + } + permEntities, err := p.ds.List(ctx, &model.Permission{Project: projectName}, &datastore.ListOptions{FilterOptions: filter}) + if err != nil { + return nil, err + } + var perms []apisv1.PermissionBase + for _, entity := range permEntities { + perm := entity.(*model.Permission) + perms = append(perms, apisv1.PermissionBase{ + Name: perm.Name, + Alias: perm.Alias, + Resources: perm.Resources, + Actions: perm.Actions, + Effect: perm.Effect, + CreateTime: perm.CreateTime, + UpdateTime: perm.UpdateTime, + }) + } + return perms, nil +} + +func (p *rbacUsecaseImpl) InitDefaultRoleAndUsersForProject(ctx context.Context, project *model.Project) error { + var batchData []datastore.Entity + for _, permissionTemp := range defaultProjectPermissionTemplate { + var rra = RequestResourceAction{} + var formatedResource []string + for _, resource := range permissionTemp.Resources { + rra.SetResourceWithName(resource, func(name string) string { + if name == ResourceMaps["project"].pathName { + return project.Name + } + return "" + }) + formatedResource = append(formatedResource, rra.GetResource().String()) + } + batchData = append(batchData, &model.Permission{ + Name: permissionTemp.Name, + Alias: permissionTemp.Alias, + Project: project.Name, + Resources: formatedResource, + Actions: permissionTemp.Actions, + Effect: permissionTemp.Effect, + }) + } + batchData = append(batchData, &model.Role{ + Name: "app-developer", + Alias: "App Developer", + Permissions: []string{"project-read", "app-management", "env-management"}, + Project: project.Name, + }, &model.Role{ + Name: "project-admin", + Alias: "Project Admin", + Permissions: []string{"project-read", "app-management", "env-management", "role-management"}, + Project: project.Name, + }) + if project.Owner != "" { + var projectUser = &model.ProjectUser{ + ProjectName: project.Name, + UserRoles: []string{"project-admin"}, + Username: project.Owner, + } + batchData = append(batchData, projectUser) + } + return p.ds.BatchAdd(ctx, batchData) +} + +// ConvertRole2Model convert role model to role base struct +func ConvertRole2Model(role *model.Role, policies []*model.Permission) *apisv1.RoleBase { + return &apisv1.RoleBase{ + CreateTime: role.CreateTime, + UpdateTime: role.UpdateTime, + Name: role.Name, + Alias: role.Alias, + Permissions: func() (list []apisv1.NameAlias) { + for _, policy := range policies { + if policy != nil { + list = append(list, apisv1.NameAlias{Name: policy.Name, Alias: policy.Alias}) + } + } + return + }(), + } +} + +// ResourceName it is similar to ARNs +// :/: +type ResourceName struct { + Type string + Value string + Next *ResourceName +} + +// ParseResourceName parse string to ResourceName +func ParseResourceName(resource string) *ResourceName { + RNs := strings.Split(resource, "/") + var resourceName = ResourceName{} + var current = &resourceName + for _, rn := range RNs { + rnData := strings.Split(rn, ":") + if len(rnData) == 2 { + current.Type = rnData[0] + current.Value = rnData[1] + } + if len(rnData) == 1 { + current.Type = rnData[0] + current.Value = "*" + } + var next = &ResourceName{} + current.Next = next + current = next + } + return &resourceName +} + +// Match the resource types same with target and resource value include +// target resource means request resources +func (r *ResourceName) Match(target *ResourceName) bool { + current := r + currentTarget := target + for current != nil && current.Type != "" { + if current.Type == "*" { + return true + } + if currentTarget == nil || currentTarget.Type == "" { + return false + } + if current.Type != currentTarget.Type { + return false + } + if current.Value != currentTarget.Value && current.Value != "*" { + return false + } + current = current.Next + currentTarget = currentTarget.Next + } + if currentTarget != nil && currentTarget.Type != "" { + return false + } + return true +} + +func (r *ResourceName) String() string { + strBuilder := &strings.Builder{} + current := r + for current != nil && current.Type != "" { + strBuilder.WriteString(fmt.Sprintf("%s:%s/", current.Type, current.Value)) + current = current.Next + } + return strings.TrimSuffix(strBuilder.String(), "/") +} + +// RequestResourceAction resource permission boundary +type RequestResourceAction struct { + resource *ResourceName + actions []string +} + +// SetResourceWithName format resource and assign a value from path parameter +func (r *RequestResourceAction) SetResourceWithName(resource string, pathParameter func(name string) string) { + resultKey := reg.FindAllString(resource, -1) + for _, sourcekey := range resultKey { + key := sourcekey[1 : len(sourcekey)-1] + value := pathParameter(key) + if value == "" { + value = "*" + } + resource = strings.Replace(resource, sourcekey, value, 1) + } + r.resource = ParseResourceName(resource) +} + +// GetResource return the resource after be formated +func (r *RequestResourceAction) GetResource() *ResourceName { + return r.resource +} + +// SetActions set request actions +func (r *RequestResourceAction) SetActions(actions []string) { + r.actions = actions +} + +func (r *RequestResourceAction) match(policy *model.Permission) bool { + // match actions, the policy actions will include the actions of request + if !utils.SliceIncludeSlice(policy.Actions, r.actions) && !utils.StringsContain(policy.Actions, "*") { + return false + } + // match resources + for _, resource := range policy.Resources { + resourceName := ParseResourceName(resource) + if resourceName.Match(r.resource) { + return true + } + } + return false +} + +// Match determines whether the request resources and actions matches the user permission set. +func (r *RequestResourceAction) Match(policies []*model.Permission) bool { + for _, policy := range policies { + if strings.EqualFold(policy.Effect, "deny") { + if r.match(policy) { + return false + } + } + } + for _, policy := range policies { + if strings.EqualFold(policy.Effect, "allow") || policy.Effect == "" { + if r.match(policy) { + return true + } + } + } + return false +} diff --git a/pkg/apiserver/rest/usecase/rbac_test.go b/pkg/apiserver/rest/usecase/rbac_test.go new file mode 100644 index 000000000..9b99183ba --- /dev/null +++ b/pkg/apiserver/rest/usecase/rbac_test.go @@ -0,0 +1,272 @@ +/* +Copyright 2022 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package usecase + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/emicklei/go-restful/v3" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +var _ = Describe("Test rbac service", func() { + var ds datastore.DataStore + BeforeEach(func() { + var err error + ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "rbac-test-kubevela"}) + Expect(ds).ToNot(BeNil()) + Expect(err).Should(BeNil()) + }) + It("Test check resource", func() { + path, err := checkResourcePath("project") + Expect(err).Should(BeNil()) + Expect(path).Should(BeEquivalentTo("project:{projectName}")) + + path, err = checkResourcePath("application") + Expect(err).Should(BeNil()) + Expect(path).Should(BeEquivalentTo("project:{projectName}/application:{appName}")) + + _, err = checkResourcePath("applications") + Expect(err).ShouldNot(BeNil()) + + _, err = checkResourcePath("project/component") + Expect(err).ShouldNot(BeNil()) + + _, err = checkResourcePath("workflow") + Expect(err).ShouldNot(BeNil()) + + path, err = checkResourcePath("project/application/workflow") + Expect(err).Should(BeNil()) + Expect(path).Should(BeEquivalentTo("project:{projectName}/application:{appName}/workflow:{workflowName}")) + + path, err = checkResourcePath("project/workflow") + Expect(err).Should(BeNil()) + Expect(path).Should(BeEquivalentTo("project:{projectName}/workflow:{workflowName}")) + + path, err = checkResourcePath("component") + Expect(err).Should(BeNil()) + Expect(path).Should(BeEquivalentTo("project:{projectName}/application:{appName}/component:{compName}")) + + path, err = checkResourcePath("role") + Expect(err).Should(BeNil()) + Expect(path).Should(BeEquivalentTo("role:*")) + + }) + + It("Test resource action", func() { + ra := &RequestResourceAction{} + ra.SetResourceWithName("project:{projectName}/workflow:{empty}", testPathParameter) + Expect(ra.GetResource()).ShouldNot(BeNil()) + Expect(ra.GetResource().Value).Should(BeEquivalentTo("projectName")) + Expect(ra.GetResource().Next).ShouldNot(BeNil()) + Expect(ra.GetResource().Next.Value).Should(BeEquivalentTo("*")) + Expect(ra.GetResource().String()).Should(BeEquivalentTo("project:projectName/workflow:*")) + }) + + It("Test init and list platform permissions", func() { + rbacUsecase := rbacUsecaseImpl{ds: ds} + err := rbacUsecase.Init(context.TODO()) + Expect(err).Should(BeNil()) + policies, err := rbacUsecase.ListPermissions(context.TODO(), "") + Expect(err).Should(BeNil()) + Expect(len(policies)).Should(BeEquivalentTo(int64(7))) + }) + + It("Test checkPerm by admin user", func() { + + err := ds.Add(context.TODO(), &model.User{Name: "admin", UserRoles: []string{"admin"}}) + Expect(err).Should(BeNil()) + + rbac := rbacUsecaseImpl{ds: ds} + req := &http.Request{} + req = req.WithContext(context.WithValue(req.Context(), &apisv1.CtxKeyUser, "admin")) + res := &restful.Response{} + pass := false + filter := &restful.FilterChain{ + Target: restful.RouteFunction(func(req *restful.Request, res *restful.Response) { + pass = true + }), + } + rbac.CheckPerm("cluster", "create")(restful.NewRequest(req), res, filter) + Expect(pass).Should(BeTrue()) + pass = false + rbac.CheckPerm("role", "list")(restful.NewRequest(req), res, filter) + Expect(pass).Should(BeTrue()) + }) + + It("Test checkPerm by dev user", func() { + var projectName = "test-app-project" + + err := ds.Add(context.TODO(), &model.User{Name: "dev"}) + Expect(err).Should(BeNil()) + + err = ds.Add(context.TODO(), &model.Project{Name: projectName}) + Expect(err).Should(BeNil()) + + err = ds.Add(context.TODO(), &model.ProjectUser{Username: "dev", ProjectName: projectName, UserRoles: []string{"application-admin"}}) + Expect(err).Should(BeNil()) + + err = ds.Add(context.TODO(), &model.Role{Project: projectName, Name: "application-admin", Permissions: []string{"application-manage"}}) + Expect(err).Should(BeNil()) + + err = ds.Add(context.TODO(), &model.Permission{Project: projectName, Name: "application-manage", Resources: []string{"project:test-app-project/application:*"}, Actions: []string{"*"}}) + Expect(err).Should(BeNil()) + + rbac := rbacUsecaseImpl{ds: ds} + header := http.Header{} + header.Set("Accept", "application/json") + header.Set("Content-Type", "application/json") + req := &http.Request{ + Header: header, + } + req = req.WithContext(context.WithValue(req.Context(), &apisv1.CtxKeyUser, "dev")) + req.Form = url.Values{} + req.Form.Set("project", projectName) + + record := httptest.NewRecorder() + res := restful.NewResponse(record) + res.SetRequestAccepts("application/json") + pass := false + filter := &restful.FilterChain{ + Target: restful.RouteFunction(func(req *restful.Request, res *restful.Response) { + pass = true + }), + } + rbac.CheckPerm("cluster", "create")(restful.NewRequest(req), res, filter) + Expect(pass).Should(BeFalse()) + Expect(res.StatusCode()).Should(Equal(int(bcode.ErrForbidden.HTTPCode))) + + rbac.CheckPerm("component", "list")(restful.NewRequest(req), res, filter) + Expect(res.StatusCode()).Should(Equal(int(bcode.ErrForbidden.HTTPCode))) + + // add list application permission to role + // err = ds.Add(context.TODO(), &model.Permission{Project: projectName, Name: "application-list", Resources: []string{"project:*/application:*"}, Actions: []string{"list"}}) + // Expect(err).Should(BeNil()) + // _, err = rbac.UpdateRole(context.TODO(), projectName, "application-admin", apisv1.UpdateRoleRequest{ + // Permissions: []string{"application-list", "application-manage"}, + // }) + // Expect(err).Should(BeNil()) + + // req.Form.Del("project") + // pass = false + // rbac.CheckPerm("application", "list")(restful.NewRequest(req), res, filter) + // Expect(pass).Should(BeTrue()) + }) + + It("Test initDefaultRoleAndUsersForProject", func() { + rbacUsecase := rbacUsecaseImpl{ds: ds} + err := ds.Add(context.TODO(), &model.User{Name: "test-user"}) + Expect(err).Should(BeNil()) + + err = ds.Add(context.TODO(), &model.Project{Name: "init-test", Owner: "test-user"}) + Expect(err).Should(BeNil()) + err = rbacUsecase.InitDefaultRoleAndUsersForProject(context.TODO(), &model.Project{Name: "init-test"}) + Expect(err).Should(BeNil()) + + roles, err := rbacUsecase.ListRole(context.TODO(), "init-test", 0, 0) + Expect(err).Should(BeNil()) + Expect(roles.Total).Should(BeEquivalentTo(int64(2))) + + policies, err := rbacUsecase.ListPermissions(context.TODO(), "init-test") + Expect(err).Should(BeNil()) + Expect(len(policies)).Should(BeEquivalentTo(int64(4))) + }) + + It("Test UpdatePermission", func() { + rbacUsecase := rbacUsecaseImpl{ds: ds} + base, err := rbacUsecase.UpdatePermission(context.TODO(), "test-app-project", "application-manage", &apisv1.UpdatePermissionRequest{ + Resources: []string{"project:{projectName}/application:*/*"}, + Actions: []string{"*"}, + Alias: "App Management Update", + }) + Expect(err).Should(BeNil()) + Expect(base.Alias).Should(BeEquivalentTo("App Management Update")) + }) +}) + +func testPathParameter(name string) string { + if name == "empty" { + return "" + } + return name +} +func TestRequestResourceAction(t *testing.T) { + ra := &RequestResourceAction{} + ra.SetResourceWithName("project:{projectName}/workflow:{empty}", testPathParameter) + assert.NotEqual(t, ra.GetResource(), nil) + assert.Equal(t, ra.GetResource().Value, "projectName") + assert.NotEqual(t, ra.GetResource().Next, nil) + assert.Equal(t, ra.GetResource().Next.Value, "*") + + ra2 := &RequestResourceAction{} + ra2.SetResourceWithName("project:{empty}/application:{empty}", testPathParameter) + assert.Equal(t, ra2.GetResource().String(), "project:*/application:*") +} + +func TestRequestResourceActionMatch(t *testing.T) { + ra := &RequestResourceAction{} + ra.SetResourceWithName("project:{projectName}/workflow:{empty}", testPathParameter) + ra.SetActions([]string{"create"}) + assert.Equal(t, ra.Match([]*model.Permission{{Resources: []string{"project:*/workflow:*"}, Actions: []string{"*"}}}), true) + assert.Equal(t, ra.Match([]*model.Permission{{Resources: []string{"project:ddd/workflow:*"}, Actions: []string{"create"}}}), false) + assert.Equal(t, ra.Match([]*model.Permission{{Resources: []string{"project:projectName/workflow:*"}, Actions: []string{"create"}}}), true) + assert.Equal(t, ra.Match([]*model.Permission{{Resources: []string{"project:projectName/workflow:*"}, Actions: []string{"create"}, Effect: "Deny"}}), false) + + ra2 := &RequestResourceAction{} + ra2.SetResourceWithName("project:{projectName}/application:{app1}/component:{empty}", testPathParameter) + ra2.SetActions([]string{"delete"}) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"project:*/application:app1/component:*"}, Actions: []string{"*"}}}), true) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"project:*/application:app1/component:*"}, Actions: []string{"list", "delete"}}}), true) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"project:*", "project:*/application:app1/component:*"}, Actions: []string{"list", "delete"}}}), true) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"project:*/application:app1/component:*"}, Actions: []string{"list", "detail"}}}), false) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"*"}, Actions: []string{"*"}}}), true) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"*"}, Actions: []string{"*"}}, {Actions: []string{"*"}, Resources: []string{"project:*/application:app1/component:*"}, Effect: "Deny"}}), false) + assert.Equal(t, ra2.Match([]*model.Permission{{Resources: []string{"project:projectName/application:*/*"}, Actions: []string{"*"}}}), true) + + ra3 := &RequestResourceAction{} + ra3.SetResourceWithName("project:test-123", testPathParameter) + ra3.SetActions([]string{"detail"}) + assert.Equal(t, ra3.Match([]*model.Permission{{Resources: []string{"*"}, Actions: []string{"*"}, Effect: "Allow"}}), true) + + ra4 := &RequestResourceAction{} + ra4.SetResourceWithName("role:*", testPathParameter) + ra4.SetActions([]string{"list"}) + assert.Equal(t, ra4.Match([]*model.Permission{{Resources: []string{"*"}, Actions: []string{"*"}, Effect: "Allow"}}), true) + + ra5 := &RequestResourceAction{} + ra5.SetResourceWithName("project:*/application:*", testPathParameter) + ra5.SetActions([]string{"list"}) + assert.Equal(t, ra5.Match([]*model.Permission{{Resources: []string{"project:*/application:*"}, Actions: []string{"list"}, Effect: "Allow"}}), true) + +} + +func TestRegisterResourceAction(t *testing.T) { + registerResourceAction("role", "list") + registerResourceAction("project/role", "list") + t.Log(resourceActions) +} diff --git a/pkg/apiserver/rest/usecase/system_info.go b/pkg/apiserver/rest/usecase/system_info.go index 8bc6876fb..61e1f4000 100644 --- a/pkg/apiserver/rest/usecase/system_info.go +++ b/pkg/apiserver/rest/usecase/system_info.go @@ -30,7 +30,6 @@ import ( // SystemInfoUsecase is usecase for systemInfoCollection type SystemInfoUsecase interface { GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error) - DeleteSystemInfo(ctx context.Context) error UpdateSystemInfo(ctx context.Context, sysInfo v1.SystemInfoRequest) (*v1.SystemInfoResponse, error) } @@ -52,6 +51,9 @@ func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInf } if len(entities) != 0 { info := entities[0].(*model.SystemInfo) + if info.LoginType == "" { + info.LoginType = model.LoginTypeLocal + } return &v1.SystemInfoResponse{SystemInfo: *info, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil } installID := rand.String(16) @@ -83,15 +85,3 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1. } return &v1.SystemInfoResponse{SystemInfo: modifiedInfo, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil } - -func (u systemInfoUsecaseImpl) DeleteSystemInfo(ctx context.Context) error { - info, err := u.GetSystemInfo(ctx) - if err != nil { - return err - } - err = u.ds.Delete(ctx, info) - if err != nil { - return err - } - return nil -} diff --git a/pkg/apiserver/rest/usecase/target.go b/pkg/apiserver/rest/usecase/target.go index 16bd4142a..28a8ca7c9 100644 --- a/pkg/apiserver/rest/usecase/target.go +++ b/pkg/apiserver/rest/usecase/target.go @@ -19,6 +19,7 @@ package usecase import ( "context" "errors" + "fmt" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,7 +39,9 @@ type TargetUsecase interface { DeleteTarget(ctx context.Context, TargetName string) error CreateTarget(ctx context.Context, req apisv1.CreateTargetRequest) (*apisv1.DetailTargetResponse, error) UpdateTarget(ctx context.Context, Target *model.Target, req apisv1.UpdateTargetRequest) (*apisv1.DetailTargetResponse, error) - ListTargets(ctx context.Context, page, pageSize int) (*apisv1.ListTargetResponse, error) + ListTargets(ctx context.Context, page, pageSize int, projectName string) (*apisv1.ListTargetResponse, error) + ListTargetCount(ctx context.Context, projectName string) (int64, error) + Init(ctx context.Context) error } type targetUsecaseImpl struct { @@ -57,20 +60,42 @@ func NewTargetUsecase(ds datastore.DataStore) TargetUsecase { ds: ds, } } - -func (dt *targetUsecaseImpl) ListTargets(ctx context.Context, page, pageSize int) (*apisv1.ListTargetResponse, error) { - - Targets, err := listTarget(ctx, dt.ds, &datastore.ListOptions{Page: page, PageSize: pageSize, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) +func (dt *targetUsecaseImpl) Init(ctx context.Context) error { + targets, err := dt.ds.List(ctx, &model.Target{}, &datastore.ListOptions{FilterOptions: datastore.FilterOptions{ + IsNotExist: []datastore.IsNotExistQueryOption{ + { + Key: "project", + }, + }, + }}) + if err != nil { + return fmt.Errorf("list target failure %w", err) + } + for _, target := range targets { + t := target.(*model.Target) + t.Project = model.DefaultInitName + if err := dt.ds.Put(ctx, t); err != nil { + return err + } + } + return nil +} +func (dt *targetUsecaseImpl) ListTargets(ctx context.Context, page, pageSize int, projectName string) (*apisv1.ListTargetResponse, error) { + targets, err := listTarget(ctx, dt.ds, projectName, &datastore.ListOptions{ + Page: page, + PageSize: pageSize, + SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}, + }) if err != nil { return nil, err } resp := &apisv1.ListTargetResponse{ Targets: []apisv1.TargetBase{}, } - for _, raw := range Targets { + for _, raw := range targets { resp.Targets = append(resp.Targets, *(dt.convertFromTargetModel(ctx, raw))) } - count, err := dt.ds.Count(ctx, &model.Target{}, nil) + count, err := dt.ds.Count(ctx, &model.Target{Project: projectName}, nil) if err != nil { return nil, err } @@ -79,9 +104,13 @@ func (dt *targetUsecaseImpl) ListTargets(ctx context.Context, page, pageSize int return resp, nil } +func (dt *targetUsecaseImpl) ListTargetCount(ctx context.Context, projectName string) (int64, error) { + return dt.ds.Count(ctx, &model.Target{Project: projectName}, nil) +} + // DeleteTarget delete application Target func (dt *targetUsecaseImpl) DeleteTarget(ctx context.Context, targetName string) error { - Target := &model.Target{ + target := &model.Target{ Name: targetName, } ddt, err := dt.GetTarget(ctx, targetName) @@ -94,7 +123,7 @@ func (dt *targetUsecaseImpl) DeleteTarget(ctx context.Context, targetName string if err = deleteTargetNamespace(ctx, dt.k8sClient, ddt.Cluster.ClusterName, ddt.Cluster.Namespace, targetName); err != nil { return err } - if err = dt.ds.Delete(ctx, Target); err != nil { + if err = dt.ds.Delete(ctx, target); err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { return bcode.ErrTargetNotExist } @@ -106,26 +135,32 @@ func (dt *targetUsecaseImpl) DeleteTarget(ctx context.Context, targetName string // CreateTarget will create a delivery target binding with a cluster and namespace, by default, it will use local cluster and namespace align with targetName // TODO(@wonderflow): we should support empty target in the future which only delivery cloud resources func (dt *targetUsecaseImpl) CreateTarget(ctx context.Context, req apisv1.CreateTargetRequest) (*apisv1.DetailTargetResponse, error) { - Target := convertCreateReqToTargetModel(req) + var project = model.Project{ + Name: req.Project, + } + if err := dt.ds.Get(ctx, &project); err != nil { + return nil, bcode.ErrProjectIsNotExist + } + target := convertCreateReqToTargetModel(req) if req.Cluster == nil { req.Cluster = &apisv1.ClusterTarget{ClusterName: multicluster.ClusterLocalName, Namespace: req.Name} } if err := createTargetNamespace(ctx, dt.k8sClient, req.Cluster.ClusterName, req.Cluster.Namespace, req.Name); err != nil { return nil, err } - err := createTarget(ctx, dt.ds, &Target) + err := createTarget(ctx, dt.ds, &target) if err != nil { return nil, err } - return dt.DetailTarget(ctx, &Target) + return dt.DetailTarget(ctx, &target) } func (dt *targetUsecaseImpl) UpdateTarget(ctx context.Context, target *model.Target, req apisv1.UpdateTargetRequest) (*apisv1.DetailTargetResponse, error) { - TargetModel := convertUpdateReqToTargetModel(target, req) - if err := dt.ds.Put(ctx, TargetModel); err != nil { + targetModel := convertUpdateReqToTargetModel(target, req) + if err := dt.ds.Put(ctx, targetModel); err != nil { return nil, err } - return dt.DetailTarget(ctx, TargetModel) + return dt.DetailTarget(ctx, targetModel) } // DetailTarget detail Target @@ -154,14 +189,15 @@ func convertUpdateReqToTargetModel(target *model.Target, req apisv1.UpdateTarget } func convertCreateReqToTargetModel(req apisv1.CreateTargetRequest) model.Target { - Target := model.Target{ + target := model.Target{ Name: req.Name, Alias: req.Alias, Description: req.Description, Cluster: (*model.ClusterTarget)(req.Cluster), Variable: req.Variable, + Project: req.Project, } - return Target + return target } func (dt *targetUsecaseImpl) convertFromTargetModel(ctx context.Context, target *model.Target) *apisv1.TargetBase { @@ -177,7 +213,15 @@ func (dt *targetUsecaseImpl) convertFromTargetModel(ctx context.Context, target UpdateTime: target.UpdateTime, AppNum: appNum, } - + if target.Project != "" { + var project = model.Project{ + Name: target.Project, + } + if err := dt.ds.Get(ctx, &project); err != nil { + log.Logger.Errorf("get project failure %s", err.Error()) + } + targetBase.Project = apisv1.NameAlias{Name: project.Name, Alias: project.Alias} + } if targetBase.Cluster != nil && targetBase.Cluster.ClusterName != "" { cluster, err := _getClusterFromDataStore(ctx, dt.ds, target.Cluster.ClusterName) if err != nil { @@ -187,6 +231,5 @@ func (dt *targetUsecaseImpl) convertFromTargetModel(ctx context.Context, target targetBase.ClusterAlias = cluster.Alias } } - return targetBase } diff --git a/pkg/apiserver/rest/usecase/target_model.go b/pkg/apiserver/rest/usecase/target_model.go index 61c7b87ef..2a5042656 100644 --- a/pkg/apiserver/rest/usecase/target_model.go +++ b/pkg/apiserver/rest/usecase/target_model.go @@ -85,13 +85,15 @@ func createTarget(ctx context.Context, ds datastore.DataStore, tg *model.Target) return nil } -func listTarget(ctx context.Context, ds datastore.DataStore, dsOption *datastore.ListOptions) ([]*model.Target, error) { +func listTarget(ctx context.Context, ds datastore.DataStore, project string, dsOption *datastore.ListOptions) ([]*model.Target, error) { if dsOption == nil { dsOption = &datastore.ListOptions{} } - - Target := model.Target{} - Targets, err := ds.List(ctx, &Target, dsOption) + target := model.Target{} + if project != "" { + target.Project = project + } + Targets, err := ds.List(ctx, &target, dsOption) if err != nil { log.Logger.Errorf("list target err %v", err) return nil, err diff --git a/pkg/apiserver/rest/usecase/target_test.go b/pkg/apiserver/rest/usecase/target_test.go index 079acd032..0428b5c56 100644 --- a/pkg/apiserver/rest/usecase/target_test.go +++ b/pkg/apiserver/rest/usecase/target_test.go @@ -38,7 +38,8 @@ var _ = Describe("Test target usecase functions", func() { ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "target-test-kubevela"}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} targetUsecase = &targetUsecaseImpl{ds: ds, k8sClient: k8sClient} }) It("Test CreateTarget function", func() { @@ -49,6 +50,7 @@ var _ = Describe("Test target usecase functions", func() { Name: "test--target", Alias: "test-alias", Description: "this is a Target", + Project: testProject, Cluster: &apisv1.ClusterTarget{ClusterName: "cluster-dev", Namespace: "dev"}, Variable: map[string]interface{}{"terraform-provider": "provider", "region": "us-1"}, } @@ -65,7 +67,7 @@ var _ = Describe("Test target usecase functions", func() { Expect(cmp.Diff(Target.Name, "test--target")).Should(BeEmpty()) By("Test ListTargets function") - resp, err := targetUsecase.ListTargets(context.TODO(), 1, 1) + resp, err := targetUsecase.ListTargets(context.TODO(), 1, 1, "") Expect(err).Should(BeNil()) Expect(resp.Targets[0].ClusterAlias).Should(Equal("dev-alias")) diff --git a/pkg/apiserver/rest/usecase/user.go b/pkg/apiserver/rest/usecase/user.go index 2f589bf96..96afd7cc6 100644 --- a/pkg/apiserver/rest/usecase/user.go +++ b/pkg/apiserver/rest/usecase/user.go @@ -18,17 +18,24 @@ package usecase import ( "context" + "errors" "golang.org/x/crypto/bcrypt" "helm.sh/helm/v3/pkg/time" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + utils2 "github.com/oam-dev/kubevela/pkg/utils" ) // UserUsecase User manage api @@ -41,18 +48,21 @@ type UserUsecase interface { ListUsers(ctx context.Context, page, pageSize int, listOptions apisv1.ListUserOptions) (*apisv1.ListUserResponse, error) DisableUser(ctx context.Context, user *model.User) error EnableUser(ctx context.Context, user *model.User) error - updateUserLoginTime(ctx context.Context, user *model.User) error + DetailLoginUserInfo(ctx context.Context) (*apisv1.LoginUserInfoResponse, error) + UpdateUserLoginTime(ctx context.Context, user *model.User) error + Init(ctx context.Context) error } type userUsecaseImpl struct { ds datastore.DataStore k8sClient client.Client projectUsecase ProjectUsecase + rbacUsecase RBACUsecase sysUsecase SystemInfoUsecase } // NewUserUsecase new User usecase -func NewUserUsecase(ds datastore.DataStore, projectUsecase ProjectUsecase, sysUsecase SystemInfoUsecase) UserUsecase { +func NewUserUsecase(ds datastore.DataStore, projectUsecase ProjectUsecase, sysUsecase SystemInfoUsecase, rbacUsecase RBACUsecase) UserUsecase { k8sClient, err := clients.GetKubeClient() if err != nil { log.Logger.Fatalf("get k8sClient failure: %s", err.Error()) @@ -62,9 +72,60 @@ func NewUserUsecase(ds datastore.DataStore, projectUsecase ProjectUsecase, sysUs ds: ds, projectUsecase: projectUsecase, sysUsecase: sysUsecase, + rbacUsecase: rbacUsecase, } } +func (u *userUsecaseImpl) Init(ctx context.Context) error { + admin := model.DefaultAdminUserName + if err := u.ds.Get(ctx, &model.User{ + Name: admin, + }); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + pwd := utils2.RandomString(8) + encrypted, err := GeneratePasswordHash(pwd) + if err != nil { + return err + } + if err := u.ds.Add(ctx, &model.User{ + Name: admin, + Alias: "Administrator", + Password: encrypted, + UserRoles: []string{"admin"}, + }); err != nil { + return err + } + // print default password of admin user in log + log.Logger.Infof("init admin user, password is %s", pwd) + secret := &corev1.Secret{} + if err := u.k8sClient.Get(ctx, k8stypes.NamespacedName{ + Name: admin, + Namespace: types.DefaultKubeVelaNS, + }, secret); err != nil { + if apierrors.IsNotFound(err) { + if err := u.k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: admin, + Namespace: types.DefaultKubeVelaNS, + }, + StringData: map[string]string{ + admin: pwd, + }, + }); err != nil { + return err + } + } else { + return err + } + } + } else { + return err + } + } + log.Logger.Info("admin user is exist") + return nil +} + // GetUser get user func (u *userUsecaseImpl) GetUser(ctx context.Context, username string) (*model.User, error) { user := &model.User{ @@ -78,7 +139,11 @@ func (u *userUsecaseImpl) GetUser(ctx context.Context, username string) (*model. // DetailUser return user detail func (u *userUsecaseImpl) DetailUser(ctx context.Context, user *model.User) (*apisv1.DetailUserResponse, error) { - detailUser := convertUserModel(user) + roles, err := u.rbacUsecase.ListRole(ctx, "", 0, 0) + if err != nil { + log.Logger.Warnf("list platform roles failure %s", err.Error()) + } + detailUser := convertUserModel(user, roles) pUser := &model.ProjectUser{ Username: user.Name, } @@ -91,16 +156,12 @@ func (u *userUsecaseImpl) DetailUser(ctx context.Context, user *model.User) (*ap for _, v := range projectUsers { pu, ok := v.(*model.ProjectUser) if ok { - project, err := u.projectUsecase.GetProject(ctx, pu.ProjectName) + project, err := u.projectUsecase.DetailProject(ctx, pu.ProjectName) if err != nil { log.Logger.Errorf("failed to delete project(%s) info: %s", pu.ProjectName, err.Error()) continue } - detailUser.Projects = append(detailUser.Projects, apisv1.ProjectUserBase{ - Name: pu.ProjectName, - Alias: project.Alias, - UserRoles: pu.UserRoles, - }) + detailUser.Projects = append(detailUser.Projects, project) } } return detailUser, nil @@ -142,12 +203,14 @@ func (u *userUsecaseImpl) CreateUser(ctx context.Context, req apisv1.CreateUserR if err != nil { return nil, err } + // TODO: validate the roles, they must be platform roles user := &model.User{ - Name: req.Name, - Alias: req.Alias, - Email: req.Email, - Password: hash, - Disabled: false, + Name: req.Name, + Alias: req.Alias, + Email: req.Email, + UserRoles: req.Roles, + Password: hash, + Disabled: false, } if err := u.ds.Add(ctx, user); err != nil { return nil, err @@ -180,6 +243,10 @@ func (u *userUsecaseImpl) UpdateUser(ctx context.Context, user *model.User, req } user.Email = req.Email } + // TODO: validate the roles, they must be platform roles + if req.Roles != nil { + user.UserRoles = *req.Roles + } if err := u.ds.Put(ctx, user); err != nil { return nil, err } @@ -211,10 +278,14 @@ func (u *userUsecaseImpl) ListUsers(ctx context.Context, page, pageSize int, lis if err != nil { return nil, err } + roles, err := u.rbacUsecase.ListRole(ctx, "", 0, 0) + if err != nil { + log.Logger.Warnf("list platform roles failure %s", err.Error()) + } for _, v := range users { user, ok := v.(*model.User) if ok { - userList = append(userList, convertUserModel(user)) + userList = append(userList, convertUserModel(user, roles)) } } count, err := u.ds.Count(ctx, user, &fo) @@ -246,16 +317,90 @@ func (u *userUsecaseImpl) EnableUser(ctx context.Context, user *model.User) erro return u.ds.Put(ctx, user) } -// updateUserLoginTime update user login time -func (u *userUsecaseImpl) updateUserLoginTime(ctx context.Context, user *model.User) error { +// UpdateUserLoginTime update user login time +func (u *userUsecaseImpl) UpdateUserLoginTime(ctx context.Context, user *model.User) error { user.LastLoginTime = time.Now().Time return u.ds.Put(ctx, user) } -func convertUserModel(user *model.User) *apisv1.DetailUserResponse { +// DetailLoginUserInfo get projects and permission policies of login user +func (u *userUsecaseImpl) DetailLoginUserInfo(ctx context.Context) (*apisv1.LoginUserInfoResponse, error) { + userName, ok := ctx.Value(&apisv1.CtxKeyUser).(string) + if !ok { + return nil, bcode.ErrUnauthorized + } + user, err := u.GetUser(ctx, userName) + if !ok { + log.Logger.Errorf("get login user model failure %s", err.Error()) + return nil, bcode.ErrUnauthorized + } + projects, err := u.projectUsecase.ListUserProjects(ctx, userName) + if err != nil { + return nil, err + } + var projectPermissions = make(map[string][]apisv1.PermissionBase) + for _, project := range projects { + perms, err := u.rbacUsecase.GetUserPermissions(ctx, user, project.Name, false) + if err != nil { + log.Logger.Errorf("list user %s perm policies from project %s failure %s", user.Name, project.Name, err.Error()) + continue + } + projectPermissions[project.Name] = func() (list []apisv1.PermissionBase) { + for _, perm := range perms { + list = append(list, apisv1.PermissionBase{ + Name: perm.Name, + Alias: perm.Alias, + Resources: perm.Resources, + Actions: perm.Actions, + Effect: perm.Effect, + CreateTime: perm.CreateTime, + UpdateTime: perm.UpdateTime, + }) + } + return + }() + } + perms, err := u.rbacUsecase.GetUserPermissions(ctx, user, "", true) + if err != nil { + log.Logger.Errorf("list user %s platform perm policies failure %s", user.Name, err.Error()) + } + var platformPermissions []apisv1.PermissionBase + for _, perm := range perms { + platformPermissions = append(platformPermissions, apisv1.PermissionBase{ + Name: perm.Name, + Alias: perm.Alias, + Resources: perm.Resources, + Actions: perm.Actions, + Effect: perm.Effect, + CreateTime: perm.CreateTime, + UpdateTime: perm.UpdateTime, + }) + } + return &apisv1.LoginUserInfoResponse{ + UserBase: *convertUserBase(user), + Projects: projects, + ProjectPermissions: projectPermissions, + PlatformPermissions: platformPermissions, + }, nil +} + +func convertUserModel(user *model.User, roles *apisv1.ListRolesResponse) *apisv1.DetailUserResponse { + + var nameAlias = make(map[string]string) + if roles != nil { + for _, role := range roles.Roles { + nameAlias[role.Name] = role.Alias + } + } return &apisv1.DetailUserResponse{ UserBase: *convertUserBase(user), - Projects: make([]apisv1.ProjectUserBase, 0), + Roles: func() (list []apisv1.NameAlias) { + for _, r := range user.UserRoles { + list = append(list, apisv1.NameAlias{Name: r, Alias: nameAlias[r]}) + } + return + }(), + Projects: make([]*apisv1.ProjectBase, 0), } } diff --git a/pkg/apiserver/rest/usecase/user_test.go b/pkg/apiserver/rest/usecase/user_test.go index 3c5b8de63..b4f4158a5 100644 --- a/pkg/apiserver/rest/usecase/user_test.go +++ b/pkg/apiserver/rest/usecase/user_test.go @@ -46,9 +46,10 @@ var _ = Describe("Test authentication usecase functions", func() { ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: db}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - projectUsecase := &projectUsecaseImpl{k8sClient: k8sClient, ds: ds} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase := &projectUsecaseImpl{k8sClient: k8sClient, ds: ds, rbacUsecase: rbacUsecase} sysUsecase := &systemInfoUsecaseImpl{ds: ds} - userUsecase = &userUsecaseImpl{ds: ds, projectUsecase: projectUsecase, sysUsecase: sysUsecase} + userUsecase = &userUsecaseImpl{ds: ds, projectUsecase: projectUsecase, sysUsecase: sysUsecase, rbacUsecase: rbacUsecase} }) AfterEach(func() { err := k8sClient.Delete(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: db}}) @@ -111,18 +112,7 @@ var _ = Describe("Test authentication usecase functions", func() { Expect(user.Name).Should(Equal("name")) Expect(user.Alias).Should(Equal("alias")) Expect(user.Email).Should(Equal("email@example.com")) - Expect(user.Projects).Should(Equal([]apisv1.ProjectUserBase{ - { - Name: "project-1", - Alias: "project-alias-1", - UserRoles: []string{"user-role-1"}, - }, - { - Name: "project-0", - Alias: "project-alias-0", - UserRoles: []string{"user-role-0"}, - }, - })) + Expect(len(user.Projects)).Should(Equal(2)) }) It("Test list users", func() { diff --git a/pkg/apiserver/rest/usecase/webhook_test.go b/pkg/apiserver/rest/usecase/webhook_test.go index 41685b5ae..79b0086d4 100644 --- a/pkg/apiserver/rest/usecase/webhook_test.go +++ b/pkg/apiserver/rest/usecase/webhook_test.go @@ -49,12 +49,13 @@ var _ = Describe("Test application usecase function", func() { ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "app-test-kubevela"}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient} workflowUsecase = &workflowUsecaseImpl{ds: ds, envUsecase: envUsecase} definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient} envBindingUsecase = &envBindingUsecaseImpl{ds: ds, envUsecase: envUsecase, workflowUsecase: workflowUsecase, kubeClient: k8sClient, definitionUsecase: definitionUsecase} targetUsecase = &targetUsecaseImpl{ds: ds, k8sClient: k8sClient} - projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient, rbacUsecase: rbacUsecase} + envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient, projectUsecase: projectUsecase} appUsecase = &applicationUsecaseImpl{ ds: ds, workflowUsecase: workflowUsecase, @@ -73,10 +74,10 @@ var _ = Describe("Test application usecase function", func() { }) It("Test HandleApplicationWebhook function", func() { - _, err := targetUsecase.CreateTarget(context.TODO(), apisv1.CreateTargetRequest{Name: "dev-target-webhook"}) + _, err := projectUsecase.CreateProject(context.TODO(), apisv1.CreateProjectRequest{Name: "project-webhook"}) Expect(err).Should(BeNil()) - _, err = projectUsecase.CreateProject(context.TODO(), apisv1.CreateProjectRequest{Name: "project-webhook"}) + _, err = targetUsecase.CreateTarget(context.TODO(), apisv1.CreateTargetRequest{Name: "dev-target-webhook", Project: "project-webhook"}) Expect(err).Should(BeNil()) _, err = envUsecase.CreateEnv(context.TODO(), apisv1.CreateEnvRequest{Name: "webhook-dev", Namespace: "webhook-dev", Targets: []string{"dev-target-webhook"}, Project: "project-webhook"}) diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index d8a918eec..5f28f15e4 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -57,8 +57,9 @@ var _ = Describe("Test workflow usecase functions", func() { ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "workflow-test-" + strconv.FormatInt(time.Now().UnixNano(), 10)}) Expect(ds).ToNot(BeNil()) Expect(err).Should(BeNil()) - projectUsecase = &projectUsecaseImpl{ds: ds} - envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient} + rbacUsecase := &rbacUsecaseImpl{ds: ds} + projectUsecase = &projectUsecaseImpl{ds: ds, rbacUsecase: rbacUsecase} + envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient, projectUsecase: projectUsecase} workflowUsecase = &workflowUsecaseImpl{ ds: ds, kubeClient: k8sClient, diff --git a/pkg/apiserver/rest/utils/bcode/003_project.go b/pkg/apiserver/rest/utils/bcode/003_project.go index 7a9b6892a..0ccf9ea42 100644 --- a/pkg/apiserver/rest/utils/bcode/003_project.go +++ b/pkg/apiserver/rest/utils/bcode/003_project.go @@ -27,3 +27,21 @@ var ErrProjectNamespaceFail = NewBcode(400, 30003, "project bind namespace failu // ErrProjectNamespaceIsExist the namespace belongs to the other project var ErrProjectNamespaceIsExist = NewBcode(400, 30004, "the namespace belongs to the other project") + +// ErrProjectDenyDeleteByApplication the project can't be deleted as there are applications inside +var ErrProjectDenyDeleteByApplication = NewBcode(400, 30005, "the project can't be deleted as there are applications inside") + +// ErrProjectDenyDeleteByEnvironment the project can't be deleted because there are environments inside +var ErrProjectDenyDeleteByEnvironment = NewBcode(400, 30006, "the project can't be deleted before you clean up all the environments inside") + +// ErrProjectDenyDeleteByTarget the project can't be deleted as there are targets inside +var ErrProjectDenyDeleteByTarget = NewBcode(400, 30007, "the project can't be deleted before you clean up all these targets inside") + +// ErrProjectRoleCheckFailure means the specified role does't belong to this project or not exist +var ErrProjectRoleCheckFailure = NewBcode(400, 30008, "the specified role does't belong to this project or not exist") + +// ErrProjectUserExist means the user is already exist in this project +var ErrProjectUserExist = NewBcode(400, 30009, "the user is already exist in this project") + +// ErrProjectOwnerIsNotExist means the project owner name is invalid +var ErrProjectOwnerIsNotExist = NewBcode(400, 30010, "the project owner name is invalid") diff --git a/pkg/apiserver/rest/utils/bcode/014_user.go b/pkg/apiserver/rest/utils/bcode/014_user.go index 15da26759..156ad9276 100644 --- a/pkg/apiserver/rest/utils/bcode/014_user.go +++ b/pkg/apiserver/rest/utils/bcode/014_user.go @@ -27,4 +27,6 @@ var ( ErrUserCannotModified = NewBcode(400, 14004, "the user cannot be modified in dex login mode") // ErrUserInvalidPassword is the error of user invalid password ErrUserInvalidPassword = NewBcode(400, 14005, "the password is invalid") + // ErrDexConfigNotFound means the dex config is not configured + ErrDexConfigNotFound = NewBcode(200, 14006, "the dex config is not found") ) diff --git a/pkg/apiserver/rest/utils/bcode/015_rbac.go b/pkg/apiserver/rest/utils/bcode/015_rbac.go new file mode 100644 index 000000000..86d8adc3c --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/015_rbac.go @@ -0,0 +1,28 @@ +/* +Copyright 2022 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bcode + +var ( + // ErrRolePermissionCheckFailure means the perm policy is invalid where create or update role + ErrRolePermissionCheckFailure = NewBcode(400, 15001, "the permissions are invalid") + // ErrRoleIsExist means the role is exist + ErrRoleIsExist = NewBcode(400, 15002, "the role name is exist") + // ErrRoleIsNotExist means the role is not exist + ErrRoleIsNotExist = NewBcode(400, 15003, "the role is not exist") + // ErrPermissionNotExist means the permission is not exist + ErrPermissionNotExist = NewBcode(404, 15004, "the permission is not exist") +) diff --git a/pkg/apiserver/rest/utils/bcode/bcode.go b/pkg/apiserver/rest/utils/bcode/bcode.go index 619adcd36..dc7829700 100644 --- a/pkg/apiserver/rest/utils/bcode/bcode.go +++ b/pkg/apiserver/rest/utils/bcode/bcode.go @@ -35,6 +35,12 @@ import ( // ErrServer an unexpected mistake. var ErrServer = NewBcode(500, 500, "The service has lapsed.") +// ErrForbidden check user perms failure +var ErrForbidden = NewBcode(403, 403, "403 Forbidden") + +// ErrUnauthorized check user auth failure +var ErrUnauthorized = NewBcode(401, 401, "401 Unauthorized") + // Bcode business error code type Bcode struct { HTTPCode int32 `json:"-"` diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 4ae7f62d5..5955db745 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -29,21 +29,24 @@ import ( ) // NewAddonWebService returns addon web service -func NewAddonWebService(u usecase.AddonHandler) WebService { +func NewAddonWebService(u usecase.AddonHandler, rbacUsecase usecase.RBACUsecase) WebService { return &addonWebService{ - handler: u, + handler: u, + rbacUsecase: rbacUsecase, } } // NewEnabledAddonWebService returns enabled addon web service -func NewEnabledAddonWebService(u usecase.AddonHandler) WebService { +func NewEnabledAddonWebService(u usecase.AddonHandler, rbacUsecase usecase.RBACUsecase) WebService { return &enabledAddonWebService{ addonUsecase: u, + rbacUsecase: rbacUsecase, } } type addonWebService struct { - handler usecase.AddonHandler + rbacUsecase usecase.RBACUsecase + handler usecase.AddonHandler } func (s *addonWebService) GetWebService() *restful.WebService { @@ -59,6 +62,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(s.listAddons). Doc("list all addons"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(s.rbacUsecase.CheckPerm("addon", "list")). Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")). Returns(200, "OK", apis.ListAddonResponse{}). @@ -66,53 +70,58 @@ func (s *addonWebService) GetWebService() *restful.WebService { Writes(apis.ListAddonResponse{})) // GET - ws.Route(ws.GET("/{name}").To(s.detailAddon). + ws.Route(ws.GET("/{addonName}").To(s.detailAddon). Doc("show details of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(s.rbacUsecase.CheckPerm("addon", "detail")). Returns(200, "OK", apis.DetailAddonResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). - Param(ws.PathParameter("name", "addon name to query detail").DataType("string").Required(true)). + Param(ws.PathParameter("addonName", "addon name to query detail").DataType("string").Required(true)). Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Writes(apis.DetailAddonResponse{})) // GET status - ws.Route(ws.GET("/{name}/status").To(s.statusAddon). + ws.Route(ws.GET("/{addonName}/status").To(s.statusAddon). Doc("show status of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(s.rbacUsecase.CheckPerm("addon", "detail")). Returns(200, "OK", apis.AddonStatusResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). - Param(ws.PathParameter("name", "addon name to query status").DataType("string").Required(true)). + Param(ws.PathParameter("addonName", "addon name to query status").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) // enable addon - ws.Route(ws.POST("/{name}/enable").To(s.enableAddon). + ws.Route(ws.POST("/{addonName}/enable").To(s.enableAddon). Doc("enable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.EnableAddonRequest{}). + Filter(s.rbacUsecase.CheckPerm("addon", "enable")). Returns(200, "OK", apis.AddonStatusResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). - Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). + Param(ws.PathParameter("addonName", "addon name to enable").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) // disable addon - ws.Route(ws.POST("/{name}/disable").To(s.disableAddon). + ws.Route(ws.POST("/{addonName}/disable").To(s.disableAddon). Doc("disable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.AddonStatusResponse{}). + Filter(s.rbacUsecase.CheckPerm("addon", "disable")). Returns(400, "Bad Request", bcode.Bcode{}). - Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). + Param(ws.PathParameter("addonName", "addon name to enable").DataType("string").Required(true)). Param(ws.QueryParameter("force", "force disable an addon").DataType("boolean").Required(false)). Writes(apis.AddonStatusResponse{})) // update addon - ws.Route(ws.PUT("/{name}/update").To(s.updateAddon). + ws.Route(ws.PUT("/{addonName}/update").To(s.updateAddon). Doc("update an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.EnableAddonRequest{}). Returns(200, "OK", apis.AddonStatusResponse{}). + Filter(s.rbacUsecase.CheckPerm("addon", "update")). Returns(400, "Bad Request", bcode.Bcode{}). - Param(ws.PathParameter("name", "addon name to update").DataType("string").Required(true)). + Param(ws.PathParameter("addonName", "addon name to update").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) ws.Filter(authCheckFilter) @@ -144,7 +153,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response } func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { - name := req.PathParameter("name") + name := req.PathParameter("addonName") addon, err := s.handler.GetAddon(req.Request.Context(), name, req.QueryParameter("registry")) if err != nil { bcode.ReturnError(req, res, err) @@ -178,8 +187,7 @@ func (s *addonWebService) enableAddon(req *restful.Request, res *restful.Respons createReq.Args[types.ClustersArg] = createReq.Clusters } - name := req.PathParameter("name") - + name := req.PathParameter("addonName") err = s.handler.EnableAddon(req.Request.Context(), name, createReq) if err != nil { bcode.ReturnError(req, res, err) @@ -190,7 +198,7 @@ func (s *addonWebService) enableAddon(req *restful.Request, res *restful.Respons } func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Response) { - name := req.PathParameter("name") + name := req.PathParameter("addonName") forceParam := req.QueryParameter("force") force, _ := strconv.ParseBool(forceParam) err := s.handler.DisableAddon(req.Request.Context(), name, force) @@ -202,7 +210,7 @@ func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Respon } func (s *addonWebService) statusAddon(req *restful.Request, res *restful.Response) { - name := req.PathParameter("name") + name := req.PathParameter("addonName") status, err := s.handler.StatusAddon(req.Request.Context(), name) if err != nil { bcode.ReturnError(req, res, err) @@ -235,7 +243,7 @@ func (s *addonWebService) updateAddon(req *restful.Request, res *restful.Respons createReq.Args[types.ClustersArg] = createReq.Clusters } - name := req.PathParameter("name") + name := req.PathParameter("addonName") err = s.handler.UpdateAddon(req.Request.Context(), name, createReq) if err != nil { bcode.ReturnError(req, res, err) @@ -247,6 +255,7 @@ func (s *addonWebService) updateAddon(req *restful.Request, res *restful.Respons type enabledAddonWebService struct { addonUsecase usecase.AddonHandler + rbacUsecase usecase.RBACUsecase } func (s *enabledAddonWebService) GetWebService() *restful.WebService { @@ -262,12 +271,14 @@ func (s *enabledAddonWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(s.list). Doc("list all addons"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(s.rbacUsecase.CheckPerm("addon", "list")). Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")). Returns(200, "OK", apis.ListAddonResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListAddonResponse{})) + ws.Filter(authCheckFilter) return ws } diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go index 32fb1374c..379b276b4 100644 --- a/pkg/apiserver/rest/webservice/addon_registry.go +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -26,14 +26,16 @@ import ( ) // NewAddonRegistryWebService returns addon registry web service -func NewAddonRegistryWebService(u usecase.AddonHandler) WebService { +func NewAddonRegistryWebService(u usecase.AddonHandler, rbacUsecase usecase.RBACUsecase) WebService { return &addonRegistryWebService{ addonUsecase: u, + rbacUsecase: rbacUsecase, } } type addonRegistryWebService struct { addonUsecase usecase.AddonHandler + rbacUsecase usecase.RBACUsecase } func (s *addonRegistryWebService) GetWebService() *restful.WebService { @@ -50,6 +52,7 @@ func (s *addonRegistryWebService) GetWebService() *restful.WebService { Doc("create an addon registry"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateAddonRegistryRequest{}). + Filter(s.rbacUsecase.CheckPerm("addonRegistry", "create")). Returns(200, "OK", apis.AddonRegistry{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.AddonRegistry{})) @@ -57,24 +60,27 @@ func (s *addonRegistryWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(s.listAddonRegistry). Doc("list all addon registry"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(s.rbacUsecase.CheckPerm("addonRegistry", "list")). Returns(200, "OK", apis.ListAddonRegistryResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListAddonRegistryResponse{})) // Delete - ws.Route(ws.DELETE("/{name}").To(s.deleteAddonRegistry). + ws.Route(ws.DELETE("/{addonRegName}").To(s.deleteAddonRegistry). Doc("delete an addon registry"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon registry").DataType("string")). + Param(ws.PathParameter("addonRegName", "identifier of the addon registry").DataType("string")). Returns(200, "OK", apis.AddonRegistry{}). + Filter(s.rbacUsecase.CheckPerm("addonRegistry", "delete")). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.AddonRegistry{})) - ws.Route(ws.PUT("/{name}").To(s.updateAddonRegistry). + ws.Route(ws.PUT("/{addonRegName}").To(s.updateAddonRegistry). Doc("update an addon registry"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.UpdateAddonRegistryRequest{}). - Param(ws.PathParameter("name", "identifier of the addon registry").DataType("string")). + Filter(s.rbacUsecase.CheckPerm("addonRegistry", "update")). + Param(ws.PathParameter("addonRegName", "identifier of the addon registry").DataType("string")). Returns(200, "OK", apis.AddonRegistry{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.AddonRegistry{})) @@ -110,7 +116,7 @@ func (s *addonRegistryWebService) createAddonRegistry(req *restful.Request, res } func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res *restful.Response) { - r, err := s.addonUsecase.GetAddonRegistry(req.Request.Context(), req.PathParameter("name")) + r, err := s.addonUsecase.GetAddonRegistry(req.Request.Context(), req.PathParameter("addonRegName")) if err != nil { bcode.ReturnError(req, res, err) return @@ -151,7 +157,7 @@ func (s *addonRegistryWebService) updateAddonRegistry(req *restful.Request, res return } // Call the usecase layer code - meta, err := s.addonUsecase.UpdateAddonRegistry(req.Request.Context(), req.PathParameter("name"), updateReq) + meta, err := s.addonUsecase.UpdateAddonRegistry(req.Request.Context(), req.PathParameter("addonRegName"), updateReq) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 16c72b46e..bfd993f00 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -33,17 +33,19 @@ import ( type applicationWebService struct { workflowWebService + rbacUsecase usecase.RBACUsecase applicationUsecase usecase.ApplicationUsecase envBindingUsecase usecase.EnvBindingUsecase } // NewApplicationWebService new application manage webservice -func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase, envBindingUsecase usecase.EnvBindingUsecase, workflowUsecase usecase.WorkflowUsecase) WebService { +func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase, envBindingUsecase usecase.EnvBindingUsecase, workflowUsecase usecase.WorkflowUsecase, rbacUsecase usecase.RBACUsecase) WebService { return &applicationWebService{ workflowWebService: workflowWebService{ workflowUsecase: workflowUsecase, applicationUsecase: applicationUsecase, }, + rbacUsecase: rbacUsecase, applicationUsecase: applicationUsecase, envBindingUsecase: envBindingUsecase, } @@ -62,8 +64,11 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Doc("list all applications"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). - Param(ws.QueryParameter("namespace", "The namespace of the managed cluster").DataType("string")). + Param(ws.QueryParameter("project", "search base on project name").DataType("string")). + Param(ws.QueryParameter("env", "search base on env name").DataType("string")). Param(ws.QueryParameter("targetName", "Name of the application delivery target").DataType("string")). + // This api will filter the app by user's permissions + // Filter(c.rbacUsecase.CheckPerm("application", "list")). Returns(200, "OK", apis.ListApplicationResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListApplicationResponse{})) @@ -72,132 +77,146 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Doc("create one application "). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateApplicationRequest{}). + Filter(c.rbacUsecase.CheckPerm("application", "create")). Returns(200, "OK", apis.ApplicationBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationBase{})) - ws.Route(ws.DELETE("/{name}").To(c.deleteApplication). + ws.Route(ws.DELETE("/{appName}").To(c.deleteApplication). Doc("delete one application"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "delete")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}").To(c.detailApplication). + ws.Route(ws.GET("/{appName}").To(c.detailApplication). Doc("detail one application "). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "detail")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.DetailApplicationResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailApplicationResponse{})) - ws.Route(ws.PUT("/{name}").To(c.updateApplication). + ws.Route(ws.PUT("/{appName}").To(c.updateApplication). Doc("update one application "). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "update")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Reads(apis.UpdateApplicationRequest{}). Returns(200, "OK", apis.ApplicationBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationBase{})) - ws.Route(ws.GET("/{name}/statistics").To(c.applicationStatistics). + ws.Route(ws.GET("/{appName}/statistics").To(c.applicationStatistics). Doc("detail one application "). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "detail")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.ApplicationStatisticsResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationStatisticsResponse{})) - ws.Route(ws.POST("/{name}/triggers").To(c.createApplicationTrigger). + ws.Route(ws.POST("/{appName}/triggers").To(c.createApplicationTrigger). Doc("create one application trigger"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("trigger", "create")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Reads(apis.CreateApplicationTriggerRequest{}). Returns(200, "OK", apis.ApplicationTriggerBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationTriggerBase{})) - ws.Route(ws.DELETE("/{name}/triggers/{token}").To(c.deleteApplicationTrigger). + ws.Route(ws.DELETE("/{appName}/triggers/{token}").To(c.deleteApplicationTrigger). Doc("delete one application trigger"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("trigger", "delete")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.PathParameter("token", "identifier of the trigger").DataType("string")). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes([]*apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}/triggers").To(c.listApplicationTriggers). + ws.Route(ws.GET("/{appName}/triggers").To(c.listApplicationTriggers). Doc("list application triggers"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("trigger", "list")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.ListApplicationTriggerResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes([]*apis.ApplicationTriggerBase{})) - ws.Route(ws.POST("/{name}/template").To(c.publishApplicationTemplate). + ws.Route(ws.POST("/{appName}/template").To(c.publishApplicationTemplate). Doc("create one application template"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("applicationTemplate", "create")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Reads(apis.CreateApplicationTemplateRequest{}). Returns(200, "OK", apis.ApplicationTemplateBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationTemplateBase{})) - ws.Route(ws.POST("/{name}/deploy").To(c.deployApplication). + ws.Route(ws.POST("/{appName}/deploy").To(c.deployApplication). Doc("deploy or upgrade the application"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "deploy")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Reads(apis.ApplicationDeployRequest{}). Returns(200, "OK", apis.ApplicationDeployResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationDeployResponse{})) - ws.Route(ws.GET("/{name}/components").To(c.listApplicationComponents). + ws.Route(ws.GET("/{appName}/components").To(c.listApplicationComponents). Doc("gets the list of application components"). + Filter(c.rbacUsecase.CheckPerm("component", "list")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.QueryParameter("envName", "list components that deployed in define env").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.ComponentListResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ComponentListResponse{})) - ws.Route(ws.POST("/{name}/components").To(c.createComponent). + ws.Route(ws.POST("/{appName}/components").To(c.createComponent). Doc("create component for application "). + Filter(c.rbacUsecase.CheckPerm("component", "create")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateComponentRequest{}). Returns(200, "OK", apis.ComponentBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ComponentBase{})) - ws.Route(ws.GET("/{name}/components/{compName}").To(c.detailComponent). + ws.Route(ws.GET("/{appName}/components/{compName}").To(c.detailComponent). Doc("detail component for application "). + Filter(c.rbacUsecase.CheckPerm("component", "detail")). Filter(c.appCheckFilter). Filter(c.componentCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.DetailComponentResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailComponentResponse{})) - ws.Route(ws.PUT("/{name}/components/{compName}").To(c.updateComponent). + ws.Route(ws.PUT("/{appName}/components/{compName}").To(c.updateComponent). Doc("update component config"). + Filter(c.rbacUsecase.CheckPerm("component", "update")). Filter(c.appCheckFilter). Filter(c.componentCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.UpdateApplicationComponentRequest{}). @@ -205,11 +224,12 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ComponentBase{})) - ws.Route(ws.DELETE("/{name}/components/{compName}").To(c.deleteComponent). + ws.Route(ws.DELETE("/{appName}/components/{compName}").To(c.deleteComponent). Doc("delete a component"). + Filter(c.rbacUsecase.CheckPerm("component", "delete")). Filter(c.appCheckFilter). Filter(c.componentCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.EmptyResponse{}). @@ -217,49 +237,54 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(404, "Not Found", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). + ws.Route(ws.GET("/{appName}/policies").To(c.listApplicationPolicies). Doc("list policy for application"). + Filter(c.rbacUsecase.CheckPerm("policy", "list")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.ListApplicationPolicy{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListApplicationPolicy{})) - ws.Route(ws.POST("/{name}/policies").To(c.createApplicationPolicy). + ws.Route(ws.POST("/{appName}/policies").To(c.createApplicationPolicy). Doc("create policy for application"). + Filter(c.rbacUsecase.CheckPerm("policy", "create")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreatePolicyRequest{}). Returns(200, "OK", apis.PolicyBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.PolicyBase{})) - ws.Route(ws.GET("/{name}/policies/{policyName}").To(c.detailApplicationPolicy). + ws.Route(ws.GET("/{appName}/policies/{policyName}").To(c.detailApplicationPolicy). Doc("detail policy for application"). + Filter(c.rbacUsecase.CheckPerm("policy", "detail")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.DetailPolicyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) - ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(c.deleteApplicationPolicy). + ws.Route(ws.DELETE("/{appName}/policies/{policyName}").To(c.deleteApplicationPolicy). Doc("detail policy for application"). + Filter(c.rbacUsecase.CheckPerm("policy", "delete")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.PUT("/{name}/policies/{policyName}").To(c.updateApplicationPolicy). + ws.Route(ws.PUT("/{appName}/policies/{policyName}").To(c.updateApplicationPolicy). Doc("update policy for application"). + Filter(c.rbacUsecase.CheckPerm("policy", "update")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.UpdatePolicyRequest{}). @@ -267,11 +292,12 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) - ws.Route(ws.POST("/{name}/components/{compName}/traits").To(c.addApplicationTrait). + ws.Route(ws.POST("/{appName}/components/{compName}/traits").To(c.addApplicationTrait). Doc("add trait for a component"). + Filter(c.rbacUsecase.CheckPerm("trait", "create")). Filter(c.appCheckFilter). Filter(c.componentCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateApplicationTraitRequest{}). @@ -279,11 +305,12 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationTrait{})) - ws.Route(ws.PUT("/{name}/components/{compName}/traits/{traitType}").To(c.updateApplicationTrait). + ws.Route(ws.PUT("/{appName}/components/{compName}/traits/{traitType}").To(c.updateApplicationTrait). Doc("update trait from a component"). + Filter(c.rbacUsecase.CheckPerm("trait", "update")). Filter(c.appCheckFilter). Filter(c.componentCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -292,11 +319,12 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationTrait{})) - ws.Route(ws.DELETE("/{name}/components/{compName}/traits/{traitType}").To(c.deleteApplicationTrait). + ws.Route(ws.DELETE("/{appName}/components/{compName}/traits/{traitType}").To(c.deleteApplicationTrait). Doc("delete trait from a component"). + Filter(c.rbacUsecase.CheckPerm("trait", "delete")). Filter(c.appCheckFilter). Filter(c.componentCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -304,10 +332,11 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}/revisions").To(c.listApplicationRevisions). + ws.Route(ws.GET("/{appName}/revisions").To(c.listApplicationRevisions). Doc("list revisions for application"). + Filter(c.rbacUsecase.CheckPerm("revision", "list")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.QueryParameter("envName", "query identifier of the env").DataType("string")). Param(ws.QueryParameter("status", "query identifier of the status").DataType("string")). Param(ws.QueryParameter("page", "query the page number").DataType("integer")). @@ -317,135 +346,148 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListRevisionsResponse{})) - ws.Route(ws.GET("/{name}/revisions/{revision}").To(c.detailApplicationRevision). + ws.Route(ws.GET("/{appName}/revisions/{revision}").To(c.detailApplicationRevision). Doc("detail revision for application"). + Filter(c.rbacUsecase.CheckPerm("revision", "detail")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application").DataType("string")). Param(ws.PathParameter("revision", "identifier of the application revision").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.DetailRevisionResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailRevisionResponse{})) - ws.Route(ws.GET("/{name}/envs").To(c.listApplicationEnvs). + ws.Route(ws.GET("/{appName}/envs").To(c.listApplicationEnvs). Doc("list policy for application"). + Filter(c.rbacUsecase.CheckPerm("envBinding", "list")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.ListApplicationEnvBinding{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListApplicationEnvBinding{})) - ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). + ws.Route(ws.POST("/{appName}/envs").To(c.createApplicationEnv). Doc("creating an application environment "). + Filter(c.rbacUsecase.CheckPerm("envBinding", "create")). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Reads(apis.CreateApplicationEnvbindingRequest{}). Returns(200, "OK", apis.EnvBinding{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.PUT("/{name}/envs/{envName}").To(c.updateApplicationEnv). + ws.Route(ws.PUT("/{appName}/envs/{envName}").To(c.updateApplicationEnv). Doc("set application differences in the specified environment"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("envBinding", "update")). Filter(c.appCheckFilter). Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.PathParameter("envName", "identifier of the envBinding ").DataType("string")). Reads(apis.PutApplicationEnvBindingRequest{}). Returns(200, "OK", apis.EnvBinding{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EnvBinding{})) - ws.Route(ws.DELETE("/{name}/envs/{envName}").To(c.deleteApplicationEnv). + ws.Route(ws.DELETE("/{appName}/envs/{envName}").To(c.deleteApplicationEnv). Doc("delete an application environment "). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("envBinding", "delete")). Filter(c.appCheckFilter). Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.PathParameter("envName", "identifier of the envBinding ").DataType("string")). Returns(200, "OK", apis.EmptyResponse{}). Returns(404, "Not Found", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}/envs/{envName}/status").To(c.getApplicationStatus). + ws.Route(ws.GET("/{appName}/envs/{envName}/status").To(c.getApplicationStatus). Doc("get application status"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("envBinding", "detail")). Filter(c.appCheckFilter). Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). Returns(200, "OK", apis.ApplicationStatusResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ApplicationStatusResponse{})) - ws.Route(ws.POST("/{name}/envs/{envName}/recycle").To(c.recycleApplicationEnv). + ws.Route(ws.POST("/{appName}/envs/{envName}/recycle").To(c.recycleApplicationEnv). Doc("get application status"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("envBinding", "recycle")). Filter(c.appCheckFilter). Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string").Required(true)). Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string").Required(true)). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}/workflows").To(c.listApplicationWorkflows). + ws.Route(ws.GET("/{appName}/workflows").To(c.listApplicationWorkflows). Doc("list application workflow"). + Filter(c.rbacUsecase.CheckPerm("application/workflow", "list")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.ListWorkflowResponse{}). Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) - ws.Route(ws.POST("/{name}/workflows").To(c.createOrUpdateApplicationWorkflow). + ws.Route(ws.POST("/{appName}/workflows").To(c.createOrUpdateApplicationWorkflow). Doc("create application workflow"). + Filter(c.rbacUsecase.CheckPerm("application/workflow", "create")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateWorkflowRequest{}). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Returns(200, "create success", apis.DetailWorkflowResponse{}). Returns(400, "create failure", bcode.Bcode{}). Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}/workflows/{workflowName}").To(c.detailWorkflow). + ws.Route(ws.GET("/{appName}/workflows/{workflowName}").To(c.detailWorkflow). Doc("detail application workflow"). + Filter(c.rbacUsecase.CheckPerm("application/workflow", "detail")). Filter(c.appCheckFilter). Filter(c.workflowCheckFilter). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workfloc.").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.workflowCheckFilter). Returns(200, "create success", apis.DetailWorkflowResponse{}). Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - ws.Route(ws.PUT("/{name}/workflows/{workflowName}").To(c.updateWorkflow). + ws.Route(ws.PUT("/{appName}/workflows/{workflowName}").To(c.updateWorkflow). Doc("update application workflow config"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application/workflow", "update")). Filter(c.appCheckFilter). Filter(c.workflowCheckFilter). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Reads(apis.UpdateWorkflowRequest{}). Returns(200, "OK", apis.DetailWorkflowResponse{}). Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - ws.Route(ws.DELETE("/{name}/workflows/{workflowName}").To(c.deleteWorkflow). + ws.Route(ws.DELETE("/{appName}/workflows/{workflowName}").To(c.deleteWorkflow). Doc("deletet workflow"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application/workflow", "delete")). Filter(c.appCheckFilter). Filter(c.workflowCheckFilter). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Returns(200, "OK", apis.EmptyResponse{}). Writes(apis.EmptyResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}/workflows/{workflowName}/records").To(c.listWorkflowRecords). + ws.Route(ws.GET("/{appName}/workflows/{workflowName}/records").To(c.listWorkflowRecords). Doc("query application workflow execution record"). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application/workflow/record", "list")). Filter(c.appCheckFilter). Filter(c.workflowCheckFilter). Param(ws.QueryParameter("page", "query the page number").DataType("integer")). @@ -453,9 +495,10 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(200, "OK", apis.ListWorkflowRecordsResponse{}). Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}").To(c.detailWorkflowRecord). + ws.Route(ws.GET("/{appName}/workflows/{workflowName}/records/{record}").To(c.detailWorkflowRecord). Doc("query application workflow execution record detail"). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Filter(c.rbacUsecase.CheckPerm("application/workflow/record", "detail")). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -464,9 +507,10 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(200, "OK", apis.DetailWorkflowRecordResponse{}). Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/resume").To(c.resumeWorkflowRecord). + ws.Route(ws.GET("/{appName}/workflows/{workflowName}/records/{record}/resume").To(c.resumeWorkflowRecord). Doc("resume suspend workflow record"). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Filter(c.rbacUsecase.CheckPerm("application/workflow/record", "resume")). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -476,9 +520,10 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailWorkflowRecordResponse{})) - ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/terminate").To(c.terminateWorkflowRecord). + ws.Route(ws.GET("/{appName}/workflows/{workflowName}/records/{record}/terminate").To(c.terminateWorkflowRecord). Doc("terminate suspend workflow record"). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Filter(c.rbacUsecase.CheckPerm("application/workflow/record", "terminate")). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -488,9 +533,10 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailWorkflowRecordResponse{})) - ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/rollback").To(c.rollbackWorkflowRecord). + ws.Route(ws.GET("/{appName}/workflows/{workflowName}/records/{record}/rollback").To(c.rollbackWorkflowRecord). Doc("rollback suspend application record"). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Filter(c.rbacUsecase.CheckPerm("application/workflow/record", "rollback")). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). Param(ws.QueryParameter("rollbackVersion", "identifier of the rollback revision").DataType("string")). @@ -501,38 +547,42 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.DetailWorkflowRecordResponse{})) - ws.Route(ws.GET("/{name}/records").To(c.listApplicationRecords). + ws.Route(ws.GET("/{appName}/records").To(c.listApplicationRecords). Doc("list application records"). - Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Filter(c.rbacUsecase.CheckPerm("application/workflow/record", "list")). + Param(ws.PathParameter("appName", "identifier of the application.").DataType("string").Required(true)). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). Returns(200, "OK", nil). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ListWorkflowRecordsResponse{})) - ws.Route(ws.POST("/{name}/compare").To(c.compareAppWithLatestRevision). + ws.Route(ws.POST("/{appName}/compare").To(c.compareAppWithLatestRevision). Doc("compare application with env latest revision"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "compare")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.ApplicationBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.AppCompareResponse{})) - ws.Route(ws.POST("/{name}/reset").To(c.resetAppToLatestRevision). + ws.Route(ws.POST("/{appName}/reset").To(c.resetAppToLatestRevision). Doc("reset application to latest revision"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "reset")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.AppResetResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.AppResetResponse{})) - ws.Route(ws.POST("/{name}/dry-run").To(c.dryRunAppOrRevision). + ws.Route(ws.POST("/{appName}/dry-run").To(c.dryRunAppOrRevision). Doc("dry-run application to latest revision"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "detail")). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("appName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.AppDryRunResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.AppDryRunResponse{})) @@ -568,8 +618,12 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res } func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { + var projetNames []string + if req.QueryParameter("project") != "" { + projetNames = append(projetNames, req.QueryParameter("project")) + } apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicationOptions{ - Project: req.QueryParameter("project"), + Projects: projetNames, Env: req.QueryParameter("env"), TargetName: req.QueryParameter("targetName"), Query: req.QueryParameter("query"), @@ -962,13 +1016,13 @@ func (c *applicationWebService) getApplicationStatus(req *restful.Request, res * } func (c *applicationWebService) listApplicationRevisions(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) if err != nil { bcode.ReturnError(req, res, err) return } - - revisions, err := c.applicationUsecase.ListRevisions(req.Request.Context(), req.PathParameter("name"), req.QueryParameter("envName"), req.QueryParameter("status"), page, pageSize) + revisions, err := c.applicationUsecase.ListRevisions(req.Request.Context(), app.Name, req.QueryParameter("envName"), req.QueryParameter("status"), page, pageSize) if err != nil { bcode.ReturnError(req, res, err) return @@ -980,7 +1034,8 @@ func (c *applicationWebService) listApplicationRevisions(req *restful.Request, r } func (c *applicationWebService) detailApplicationRevision(req *restful.Request, res *restful.Response) { - detail, err := c.applicationUsecase.DetailRevision(req.Request.Context(), req.PathParameter("name"), req.PathParameter("revision")) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailRevision(req.Request.Context(), app.Name, req.PathParameter("revision")) if err != nil { bcode.ReturnError(req, res, err) return @@ -1064,7 +1119,7 @@ func (c *applicationWebService) deleteApplicationEnv(req *restful.Request, res * } func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) + app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("appName")) if err != nil { bcode.ReturnError(req, res, err) return @@ -1123,7 +1178,8 @@ func (c *applicationWebService) recycleApplicationEnv(req *restful.Request, res } func (c *applicationWebService) listApplicationRecords(req *restful.Request, res *restful.Response) { - records, err := c.applicationUsecase.ListRecords(req.Request.Context(), req.PathParameter("name")) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + records, err := c.applicationUsecase.ListRecords(req.Request.Context(), app.Name) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/authentication.go b/pkg/apiserver/rest/webservice/authentication.go index 4a60bf37d..ace5354cc 100644 --- a/pkg/apiserver/rest/webservice/authentication.go +++ b/pkg/apiserver/rest/webservice/authentication.go @@ -30,12 +30,14 @@ import ( type authenticationWebService struct { authenticationUsecase usecase.AuthenticationUsecase + userUsecase usecase.UserUsecase } // NewAuthenticationWebService is the webservice of authentication -func NewAuthenticationWebService(authenticationUsecase usecase.AuthenticationUsecase) WebService { +func NewAuthenticationWebService(authenticationUsecase usecase.AuthenticationUsecase, userUsecase usecase.UserUsecase) WebService { return &authenticationWebService{ authenticationUsecase: authenticationUsecase, + userUsecase: userUsecase, } } @@ -56,26 +58,34 @@ func (c *authenticationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.LoginResponse{})) - ws.Route(ws.GET("/dexConfig").To(c.getDexConfig). + ws.Route(ws.GET("/dex_config").To(c.getDexConfig). Doc("get Dex config"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.DexConfigResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.DexConfigResponse{})) - ws.Route(ws.GET("/refreshToken").To(c.refreshToken). + ws.Route(ws.GET("/refresh_token").To(c.refreshToken). Doc("refresh token"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.RefreshTokenResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.RefreshTokenResponse{})) - ws.Route(ws.GET("/loginType").To(c.getLoginType). + ws.Route(ws.GET("/login_type").To(c.getLoginType). Doc("get login type"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.GetLoginTypeResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.GetLoginTypeResponse{})) + + ws.Route(ws.GET("/user_info").To(c.getLoginUserInfo). + Doc("get login user detail info"). + Filter(authCheckFilter). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.LoginUserInfoResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.LoginUserInfoResponse{})) return ws } @@ -157,3 +167,15 @@ func (c *authenticationWebService) getLoginType(req *restful.Request, res *restf return } } + +func (c *authenticationWebService) getLoginUserInfo(req *restful.Request, res *restful.Response) { + info, err := c.userUsecase.DetailLoginUserInfo(req.Request.Context()) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(info); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 7a4920ce4..1fc59b6b3 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -29,11 +29,12 @@ import ( // ClusterWebService cluster manage webservice type ClusterWebService struct { clusterUsecase usecase.ClusterUsecase + rbacUsecase usecase.RBACUsecase } // NewClusterWebService new cluster webservice -func NewClusterWebService(clusterUsecase usecase.ClusterUsecase) *ClusterWebService { - return &ClusterWebService{clusterUsecase: clusterUsecase} +func NewClusterWebService(clusterUsecase usecase.ClusterUsecase, rbacUsecase usecase.RBACUsecase) *ClusterWebService { + return &ClusterWebService{clusterUsecase: clusterUsecase, rbacUsecase: rbacUsecase} } // GetWebService - @@ -49,6 +50,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(c.listKubeClusters). Doc("list all clusters"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "list")). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). Param(ws.QueryParameter("page", "Page for paging").DataType("integer").DefaultValue("0")). Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("integer").DefaultValue("20")). @@ -60,6 +62,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Doc("create cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateClusterRequest{}). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Returns(200, "OK", apis.ClusterBase{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.ClusterBase{})) @@ -67,6 +70,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{clusterName}").To(c.getKubeCluster). Doc("detail cluster info"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "detail")). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). Returns(200, "OK", apis.DetailClusterResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). @@ -75,6 +79,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.PUT("/{clusterName}").To(c.modifyKubeCluster). Doc("modify cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "update")). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). Reads(apis.CreateClusterRequest{}). Returns(200, "OK", apis.ClusterBase{}). @@ -84,6 +89,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.DELETE("/{clusterName}").To(c.deleteKubeCluster). Doc("delete cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "delete")). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). Returns(200, "OK", apis.ClusterBase{}). Returns(400, "Bad Request", bcode.Bcode{}). @@ -94,6 +100,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("clusterName", "name of the target cluster").DataType("string")). Reads(apis.CreateClusterNamespaceRequest{}). + Filter(c.rbacUsecase.CheckPerm("cluster/namespace", "create")). Returns(200, "OK", apis.CreateClusterNamespaceResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.CreateClusterNamespaceResponse{})) @@ -101,6 +108,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/cloud_clusters/{provider}").To(c.listCloudClusters). Doc("list cloud clusters"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Param(ws.QueryParameter("page", "Page for paging").DataType("integer").DefaultValue("0")). Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("integer").DefaultValue("20")). @@ -112,6 +120,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/cloud_clusters/{provider}/connect").To(c.connectCloudCluster). Doc("create cluster from cloud cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Reads(apis.ConnectCloudClusterRequest{}). Returns(200, "OK", apis.ClusterBase{}). @@ -121,6 +130,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/cloud_clusters/{provider}/create").To(c.createCloudCluster). Doc("create cloud cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string").Required(true)). Reads(apis.CreateCloudClusterRequest{}). Returns(200, "OK", apis.CreateCloudClusterResponse{}). @@ -130,6 +140,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/cloud_clusters/{provider}/creation/{cloudClusterName}").To(c.getCloudClusterCreationStatus). Doc("check cloud cluster create status"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Param(ws.PathParameter("cloudClusterName", "identifier for cloud cluster which is creating").DataType("string")). Returns(200, "OK", apis.CreateCloudClusterResponse{}). @@ -138,6 +149,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/cloud_clusters/{provider}/creation").To(c.listCloudClusterCreation). Doc("list cloud cluster creation"). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Returns(200, "OK", apis.ListCloudClusterCreationResponse{}). @@ -147,6 +159,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.DELETE("/cloud_clusters/{provider}/creation/{cloudClusterName}").To(c.deleteCloudClusterCreation). Doc("delete cloud cluster creation"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("cluster", "create")). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Param(ws.PathParameter("cloudClusterName", "identifier for cloud cluster which is creating").DataType("string")). Returns(200, "OK", apis.CreateCloudClusterResponse{}). diff --git a/pkg/apiserver/rest/webservice/definition.go b/pkg/apiserver/rest/webservice/definition.go index 8badf3254..bb0d90390 100644 --- a/pkg/apiserver/rest/webservice/definition.go +++ b/pkg/apiserver/rest/webservice/definition.go @@ -27,6 +27,7 @@ import ( type definitionWebservice struct { definitionUsecase usecase.DefinitionUsecase + rbacUsecase usecase.RBACUsecase } func (d *definitionWebservice) GetWebService() *restful.WebService { @@ -41,15 +42,18 @@ func (d *definitionWebservice) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(d.listDefinitions). Doc("list all definitions"). Metadata(restfulspec.KeyOpenAPITags, tags). + // TODO: provide project scope api for query definition list + // Filter(d.rbacUsecase.CheckPerm("definition", "list")). Param(ws.QueryParameter("type", "query the definition type").DataType("string").Required(true).AllowableValues(map[string]string{"component": "", "trait": "", "workflowstep": ""})). Param(ws.QueryParameter("envName", "if specified, query the definition supported by the env.").DataType("string")). Param(ws.QueryParameter("appliedWorkload", "if specified, query the trait definition applied to the workload.").DataType("string")). Returns(200, "OK", apis.ListDefinitionResponse{}). Writes(apis.ListDefinitionResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}").To(d.detailDefinition). + ws.Route(ws.GET("/{definitionName}").To(d.detailDefinition). Doc("detail definition"). - Param(ws.PathParameter("name", "identifier of the definition").DataType("string")). + // Filter(d.rbacUsecase.CheckPerm("definition", "detail")). + Param(ws.PathParameter("definitionName", "identifier of the definition").DataType("string")). Param(ws.QueryParameter("type", "query the definition type").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "create success", apis.DetailDefinitionResponse{}). @@ -60,9 +64,10 @@ func (d *definitionWebservice) GetWebService() *restful.WebService { } // NewDefinitionWebservice new definition webservice -func NewDefinitionWebservice(du usecase.DefinitionUsecase) WebService { +func NewDefinitionWebservice(du usecase.DefinitionUsecase, rbacUsecase usecase.RBACUsecase) WebService { return &definitionWebservice{ definitionUsecase: du, + rbacUsecase: rbacUsecase, } } @@ -79,7 +84,7 @@ func (d *definitionWebservice) listDefinitions(req *restful.Request, res *restfu } func (d *definitionWebservice) detailDefinition(req *restful.Request, res *restful.Response) { - definition, err := d.definitionUsecase.DetailDefinition(req.Request.Context(), req.PathParameter("name"), req.QueryParameter("type")) + definition, err := d.definitionUsecase.DetailDefinition(req.Request.Context(), req.PathParameter("definitionName"), req.QueryParameter("type")) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/env.go b/pkg/apiserver/rest/webservice/env.go index 2fb7e8eb5..10285acbe 100644 --- a/pkg/apiserver/rest/webservice/env.go +++ b/pkg/apiserver/rest/webservice/env.go @@ -28,13 +28,14 @@ import ( ) type envWebService struct { - envUsecase usecase.EnvUsecase - appUsecase usecase.ApplicationUsecase + envUsecase usecase.EnvUsecase + appUsecase usecase.ApplicationUsecase + rbacUsecase usecase.RBACUsecase } // NewEnvWebService new env webservice -func NewEnvWebService(envUsecase usecase.EnvUsecase, appUseCase usecase.ApplicationUsecase) WebService { - return &envWebService{envUsecase: envUsecase, appUsecase: appUseCase} +func NewEnvWebService(envUsecase usecase.EnvUsecase, appUseCase usecase.ApplicationUsecase, rbacUsecase usecase.RBACUsecase) WebService { + return &envWebService{envUsecase: envUsecase, appUsecase: appUseCase, rbacUsecase: rbacUsecase} } func (n *envWebService) GetWebService() *restful.WebService { @@ -49,6 +50,8 @@ func (n *envWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(n.list). Operation("envlist"). Doc("list all envs"). + // This api will filter the environments by user's permissions + // Filter(n.rbacUsecase.CheckPerm("environment", "list")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "OK", apis.ListEnvResponse{}). Writes(apis.ListEnvResponse{})) @@ -56,25 +59,28 @@ func (n *envWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/").To(n.create). Operation("envcreate"). Doc("create an env"). + Filter(n.rbacUsecase.CheckPerm("environment", "create")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateEnvRequest{}). Returns(200, "OK", apis.Env{}). Writes(apis.Env{})) - ws.Route(ws.PUT("/{name}").To(n.update). + ws.Route(ws.PUT("/{envName}").To(n.update). Operation("envupdate"). Doc("update an env"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("environment", "update")). + Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). Reads(apis.CreateEnvRequest{}). Returns(200, "OK", apis.Env{}). Writes(apis.Env{})) - ws.Route(ws.DELETE("/{name}").To(n.delete). + ws.Route(ws.DELETE("/{envName}").To(n.delete). Operation("envdelete"). Doc("delete one env"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("environment", "delete")). + Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) @@ -103,7 +109,7 @@ func (n *envWebService) list(req *restful.Request, res *restful.Response) { // it will prevent the deletion if there's still application in it. func (n *envWebService) delete(req *restful.Request, res *restful.Response) { - envname := req.PathParameter("name") + envname := req.PathParameter("envName") ctx := req.Request.Context() lists, err := n.appUsecase.ListApplications(ctx, apis.ListApplicationOptions{Env: envname}) @@ -166,7 +172,7 @@ func (n *envWebService) update(req *restful.Request, res *restful.Response) { return } - env, err := n.envUsecase.UpdateEnv(req.Request.Context(), req.PathParameter("name"), updateReq) + env, err := n.envUsecase.UpdateEnv(req.Request.Context(), req.PathParameter("envName"), updateReq) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/oam_application.go b/pkg/apiserver/rest/webservice/oam_application.go index 9bc908a18..5b57ea6f5 100644 --- a/pkg/apiserver/rest/webservice/oam_application.go +++ b/pkg/apiserver/rest/webservice/oam_application.go @@ -28,12 +28,14 @@ import ( type oamApplicationWebService struct { oamApplicationUsecase usecase.OAMApplicationUsecase + rbacUsecase usecase.RBACUsecase } // NewOAMApplication new oam application -func NewOAMApplication(oamApplicationUsecase usecase.OAMApplicationUsecase) WebService { +func NewOAMApplication(oamApplicationUsecase usecase.OAMApplicationUsecase, rbacUsecase usecase.RBACUsecase) WebService { return &oamApplicationWebService{ oamApplicationUsecase: oamApplicationUsecase, + rbacUsecase: rbacUsecase, } } @@ -49,6 +51,7 @@ func (c *oamApplicationWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/namespaces/{namespace}/applications/{appname}").To(c.getApplication). Doc("get the specified oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "detail")). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). Returns(200, "OK", apis.ApplicationResponse{}). @@ -57,6 +60,7 @@ func (c *oamApplicationWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/namespaces/{namespace}/applications/{appname}").To(c.createOrUpdateApplication). Doc("create or update oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "deploy")). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). Reads(apis.ApplicationRequest{})) @@ -65,9 +69,11 @@ func (c *oamApplicationWebService) GetWebService() *restful.WebService { Operation("deleteOAMApplication"). Doc("create or update oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("application", "delete")). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string"))) + ws.Filter(authCheckFilter) return ws } diff --git a/pkg/apiserver/rest/webservice/project.go b/pkg/apiserver/rest/webservice/project.go index c739d50c4..1c1bd3115 100644 --- a/pkg/apiserver/rest/webservice/project.go +++ b/pkg/apiserver/rest/webservice/project.go @@ -28,12 +28,14 @@ import ( ) type projectWebService struct { + rbacUsecase usecase.RBACUsecase projectUsecase usecase.ProjectUsecase + targetUsecase usecase.TargetUsecase } // NewProjectWebService new project webservice -func NewProjectWebService(projectUsecase usecase.ProjectUsecase) WebService { - return &projectWebService{projectUsecase: projectUsecase} +func NewProjectWebService(projectUsecase usecase.ProjectUsecase, rbacUsecase usecase.RBACUsecase, targetUsecase usecase.TargetUsecase) WebService { + return &projectWebService{projectUsecase: projectUsecase, rbacUsecase: rbacUsecase, targetUsecase: targetUsecase} } func (n *projectWebService) GetWebService() *restful.WebService { @@ -48,16 +50,132 @@ func (n *projectWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(n.listprojects). Doc("list all projects"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(n.rbacUsecase.CheckPerm("project", "list")). Returns(200, "OK", apis.ListProjectResponse{}). Writes(apis.ListProjectResponse{})) ws.Route(ws.POST("/").To(n.createproject). Doc("create a project"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(n.rbacUsecase.CheckPerm("project", "create")). Reads(apis.CreateProjectRequest{}). Returns(200, "OK", apis.ProjectBase{}). Writes(apis.ProjectBase{})) + ws.Route(ws.GET("/{projectName}").To(n.detailProject). + Doc("detail a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project", "detail")). + Returns(200, "OK", apis.ProjectBase{}). + Writes(apis.ProjectBase{})) + + ws.Route(ws.PUT("/{projectName}").To(n.updateProject). + Doc("update a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project", "update")). + Reads(apis.UpdateProjectRequest{}). + Returns(200, "OK", apis.ProjectBase{}). + Writes(apis.ProjectBase{})) + + ws.Route(ws.DELETE("/{projectName}").To(n.deleteProject). + Doc("delete a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project", "delete")). + Returns(200, "OK", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{projectName}/targets").To(n.listProjectTargets). + Doc("get targets list belong to a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project", "detail")). + Returns(200, "OK", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.POST("/{projectName}/users").To(n.createProjectUser). + Doc("add a user to a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/projectUser", "create")). + Reads(apis.AddProjectUserRequest{}). + Returns(200, "OK", apis.ProjectUserBase{}). + Writes(apis.ProjectUserBase{})) + + ws.Route(ws.GET("/{projectName}/users").To(n.listProjectUser). + Doc("list all users belong to a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/projectUser", "list")). + Returns(200, "OK", apis.ListProjectUsersResponse{}). + Writes(apis.ListProjectUsersResponse{})) + + ws.Route(ws.PUT("/{projectName}/users/{userName}").To(n.updateProjectUser). + Doc("add a user to a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateProjectUserRequest{}). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Param(ws.PathParameter("userName", "identifier of the project user").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/projectUser", "create")). + Returns(200, "OK", apis.ProjectUserBase{}). + Writes(apis.ProjectUserBase{})) + + ws.Route(ws.DELETE("/{projectName}/users/{userName}").To(n.deleteProjectUser). + Doc("delete a user from a project"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateProjectUserRequest{}). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Param(ws.PathParameter("userName", "identifier of the project user").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/projectUser", "delete")). + Returns(200, "OK", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{projectName}/roles").To(n.listProjectRoles). + Doc("list all project level roles"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/role", "list")). + Returns(200, "OK", apis.ListRolesResponse{}). + Writes(apis.ListRolesResponse{})) + + ws.Route(ws.POST("/{projectName}/roles").To(n.createProjectRole). + Doc("create project level role"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/role", "create")). + Returns(200, "OK", apis.RoleBase{}). + Reads(apis.CreateRoleRequest{}). + Writes(apis.RoleBase{})) + + ws.Route(ws.PUT("/{projectName}/roles/{roleName}").To(n.updateProjectRole). + Doc("update project level role"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Param(ws.PathParameter("roleName", "identifier of the project role").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/role", "update")). + Reads(apis.UpdateRoleRequest{}). + Returns(200, "OK", apis.RoleBase{}). + Writes(apis.RoleBase{})) + + ws.Route(ws.DELETE("/{projectName}/roles/{roleName}").To(n.deleteProjectRole). + Doc("delete project level role"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Param(ws.PathParameter("roleName", "identifier of the project role").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/role", "delete")). + Returns(200, "OK", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{projectName}/permissions").To(n.listProjectPermissions). + Doc("list all project level perm policies"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")). + Filter(n.rbacUsecase.CheckPerm("project/permission", "list")). + Returns(200, "OK", []apis.PermissionBase{}). + Writes([]apis.PermissionBase{})) + ws.Filter(authCheckFilter) return ws } @@ -93,7 +211,7 @@ func (n *projectWebService) createproject(req *restful.Request, res *restful.Res // Call the usecase layer code projectBase, err := n.projectUsecase.CreateProject(req.Request.Context(), createReq) if err != nil { - log.Logger.Errorf("create application failure %s", err.Error()) + log.Logger.Errorf("create project failure %s", err.Error()) bcode.ReturnError(req, res, err) return } @@ -104,3 +222,284 @@ func (n *projectWebService) createproject(req *restful.Request, res *restful.Res return } } + +func (n *projectWebService) updateProject(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var updateReq apis.UpdateProjectRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + projectBase, err := n.projectUsecase.UpdateProject(req.Request.Context(), req.PathParameter("projectName"), updateReq) + if err != nil { + log.Logger.Errorf("update project failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(projectBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) detailProject(req *restful.Request, res *restful.Response) { + project, err := n.projectUsecase.DetailProject(req.Request.Context(), req.PathParameter("projectName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(project); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) deleteProject(req *restful.Request, res *restful.Response) { + err := n.projectUsecase.DeleteProject(req.Request.Context(), req.PathParameter("projectName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) listProjectTargets(req *restful.Request, res *restful.Response) { + project, err := n.projectUsecase.GetProject(req.Request.Context(), req.PathParameter("projectName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + projects, err := n.targetUsecase.ListTargets(req.Request.Context(), 0, 0, project.Name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(projects); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) createProjectUser(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.AddProjectUserRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if len(createReq.UserRoles) == 0 { + bcode.ReturnError(req, res, bcode.ErrProjectRoleCheckFailure) + return + } + // Call the usecase layer code + userBase, err := n.projectUsecase.AddProjectUser(req.Request.Context(), req.PathParameter("projectName"), createReq) + if err != nil { + log.Logger.Errorf("create project user failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(userBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) listProjectUser(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + users, err := n.projectUsecase.ListProjectUser(req.Request.Context(), req.PathParameter("projectName"), page, pageSize) + if err != nil { + log.Logger.Errorf("list project users failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(users); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) updateProjectUser(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var updateReq apis.UpdateProjectUserRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if len(updateReq.UserRoles) == 0 { + bcode.ReturnError(req, res, bcode.ErrProjectRoleCheckFailure) + return + } + // Call the usecase layer code + userBase, err := n.projectUsecase.UpdateProjectUser(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("userName"), updateReq) + if err != nil { + log.Logger.Errorf("update project user failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(userBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) deleteProjectUser(req *restful.Request, res *restful.Response) { + // Call the usecase layer code + err := n.projectUsecase.DeleteProjectUser(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("userName")) + if err != nil { + log.Logger.Errorf("delete project user failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) listProjectRoles(req *restful.Request, res *restful.Response) { + if req.PathParameter("projectName") == "" { + bcode.ReturnError(req, res, bcode.ErrProjectIsNotExist) + return + } + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + roles, err := n.rbacUsecase.ListRole(req.Request.Context(), req.PathParameter("projectName"), page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(roles); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) createProjectRole(req *restful.Request, res *restful.Response) { + if req.PathParameter("projectName") == "" { + bcode.ReturnError(req, res, bcode.ErrProjectIsNotExist) + return + } + // Verify the validity of parameters + var createReq apis.CreateRoleRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + projectBase, err := n.rbacUsecase.CreateRole(req.Request.Context(), req.PathParameter("projectName"), createReq) + if err != nil { + log.Logger.Errorf("create role failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(projectBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) updateProjectRole(req *restful.Request, res *restful.Response) { + if req.PathParameter("projectName") == "" { + bcode.ReturnError(req, res, bcode.ErrProjectIsNotExist) + return + } + // Verify the validity of parameters + var updateReq apis.UpdateRoleRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + roleBase, err := n.rbacUsecase.UpdateRole(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("roleName"), updateReq) + if err != nil { + log.Logger.Errorf("update role failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(roleBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) deleteProjectRole(req *restful.Request, res *restful.Response) { + if req.PathParameter("projectName") == "" { + bcode.ReturnError(req, res, bcode.ErrProjectIsNotExist) + return + } + err := n.rbacUsecase.DeleteRole(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("roleName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *projectWebService) listProjectPermissions(req *restful.Request, res *restful.Response) { + if req.PathParameter("projectName") == "" { + bcode.ReturnError(req, res, bcode.ErrProjectIsNotExist) + return + } + policies, err := n.rbacUsecase.ListPermissions(req.Request.Context(), req.PathParameter("projectName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(policies); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/rbac.go b/pkg/apiserver/rest/webservice/rbac.go new file mode 100644 index 000000000..a89b517ac --- /dev/null +++ b/pkg/apiserver/rest/webservice/rbac.go @@ -0,0 +1,181 @@ +/* +Copyright 2022 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webservice + +import ( + restfulspec "github.com/emicklei/go-restful-openapi/v2" + "github.com/emicklei/go-restful/v3" + + "github.com/oam-dev/kubevela/pkg/apiserver/log" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +type rbacWebService struct { + rbacUsecase usecase.RBACUsecase +} + +// NewRBACWebService new rbac webservice +func NewRBACWebService(rbacUsecase usecase.RBACUsecase) WebService { + return &rbacWebService{rbacUsecase: rbacUsecase} +} + +func (r *rbacWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for rbac") + + tags := []string{"rbac"} + + ws.Route(ws.GET("/roles").To(r.listPlatformRoles). + Doc("list all platform level roles"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(r.rbacUsecase.CheckPerm("role", "list")). + Returns(200, "OK", apis.ListRolesResponse{}). + Writes(apis.ListRolesResponse{})) + + ws.Route(ws.POST("/roles").To(r.createPlatformRole). + Doc("create platform level role"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(r.rbacUsecase.CheckPerm("role", "create")). + Returns(200, "OK", apis.RoleBase{}). + Reads(apis.CreateRoleRequest{}). + Writes(apis.RoleBase{})) + + ws.Route(ws.PUT("/roles/{roleName}").To(r.updatePlatformRole). + Doc("update platform level role"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(r.rbacUsecase.CheckPerm("role", "update")). + Reads(apis.UpdateRoleRequest{}). + Returns(200, "OK", apis.RoleBase{}). + Writes(apis.RoleBase{})) + + ws.Route(ws.DELETE("/roles/{roleName}").To(r.deletePlatformRole). + Doc("update platform level role"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(r.rbacUsecase.CheckPerm("role", "delete")). + Returns(200, "OK", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/permissions").To(r.listPlatformPermissions). + Doc("list all project level perm policies"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(r.rbacUsecase.CheckPerm("permission", "list")). + Returns(200, "OK", []apis.PermissionBase{}). + Writes([]apis.PermissionBase{})) + + ws.Filter(authCheckFilter) + return ws +} + +func (r *rbacWebService) listPlatformRoles(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + roles, err := r.rbacUsecase.ListRole(req.Request.Context(), "", page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(roles); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (r *rbacWebService) createPlatformRole(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateRoleRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + projectBase, err := r.rbacUsecase.CreateRole(req.Request.Context(), "", createReq) + if err != nil { + log.Logger.Errorf("create role failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(projectBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (r *rbacWebService) updatePlatformRole(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var updateReq apis.UpdateRoleRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + roleBase, err := r.rbacUsecase.UpdateRole(req.Request.Context(), "", req.PathParameter("roleName"), updateReq) + if err != nil { + log.Logger.Errorf("update role failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(roleBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (r *rbacWebService) deletePlatformRole(req *restful.Request, res *restful.Response) { + err := r.rbacUsecase.DeleteRole(req.Request.Context(), "", req.PathParameter("roleName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (r *rbacWebService) listPlatformPermissions(req *restful.Request, res *restful.Response) { + policies, err := r.rbacUsecase.ListPermissions(req.Request.Context(), "") + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(policies); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/system_info.go b/pkg/apiserver/rest/webservice/system_info.go index 8041818ae..958ffeba5 100644 --- a/pkg/apiserver/rest/webservice/system_info.go +++ b/pkg/apiserver/rest/webservice/system_info.go @@ -26,12 +26,13 @@ import ( ) type systemInfoWebService struct { - useCase usecase.SystemInfoUsecase + useCase usecase.SystemInfoUsecase + rbacUsecase usecase.RBACUsecase } // NewSystemInfoWebService return systemInfo webservice -func NewSystemInfoWebService(systemInfoUseCase usecase.SystemInfoUsecase) WebService { - return &systemInfoWebService{useCase: systemInfoUseCase} +func NewSystemInfoWebService(systemInfoUseCase usecase.SystemInfoUsecase, rbacUsecase usecase.RBACUsecase) WebService { + return &systemInfoWebService{useCase: systemInfoUseCase, rbacUsecase: rbacUsecase} } // GetWebService return systemInfo webservice @@ -50,17 +51,11 @@ func (u systemInfoWebService) GetWebService() *restful.WebService { Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.SystemInfoResponse{})) - // Delete - ws.Route(ws.DELETE("/").To(u.deleteSystemInfo). - Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "OK", apis.SystemInfoResponse{}). - Returns(400, "Bad Request", bcode.Bcode{}). - Writes(apis.SystemInfoResponse{})) - // Post ws.Route(ws.PUT("/").To(u.updateSystemInfo). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.SystemInfoRequest{}). + Filter(u.rbacUsecase.CheckPerm("systemSetting", "update")). Returns(200, "OK", apis.SystemInfoResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.SystemInfoResponse{})) @@ -107,11 +102,3 @@ func (u systemInfoWebService) updateSystemInfo(req *restful.Request, res *restfu return } } - -func (u systemInfoWebService) deleteSystemInfo(req *restful.Request, res *restful.Response) { - err := u.useCase.DeleteSystemInfo(req.Request.Context()) - if err != nil { - bcode.ReturnError(req, res, err) - return - } -} diff --git a/pkg/apiserver/rest/webservice/target.go b/pkg/apiserver/rest/webservice/target.go index 0636b6d7c..bd9c2a816 100644 --- a/pkg/apiserver/rest/webservice/target.go +++ b/pkg/apiserver/rest/webservice/target.go @@ -35,10 +35,11 @@ import ( ) // NewTargetWebService new Target webservice -func NewTargetWebService(targetUsecase usecase.TargetUsecase, applicationUsecase usecase.ApplicationUsecase) WebService { +func NewTargetWebService(targetUsecase usecase.TargetUsecase, applicationUsecase usecase.ApplicationUsecase, rbacUsecase usecase.RBACUsecase) WebService { return &TargetWebService{ TargetUsecase: targetUsecase, applicationUsecase: applicationUsecase, + rbacUsecase: rbacUsecase, } } @@ -46,6 +47,7 @@ func NewTargetWebService(targetUsecase usecase.TargetUsecase, applicationUsecase type TargetWebService struct { TargetUsecase usecase.TargetUsecase applicationUsecase usecase.ApplicationUsecase + rbacUsecase usecase.RBACUsecase } // GetWebService get web service @@ -61,8 +63,10 @@ func (dt *TargetWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(dt.listTargets). Doc("list Target"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.rbacUsecase.CheckPerm("target", "list")). Param(ws.QueryParameter("page", "Page for paging").DataType("integer")). Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("integer")). + Param(ws.QueryParameter("project", "list targets by project name").DataType("string")). Returns(200, "OK", apis.ListTargetResponse{}). Writes(apis.ListTargetResponse{}).Do(returns200, returns500)) @@ -70,32 +74,36 @@ func (dt *TargetWebService) GetWebService() *restful.WebService { Doc("create Target"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateTargetRequest{}). + Filter(dt.rbacUsecase.CheckPerm("target", "create")). Returns(200, "create success", apis.DetailTargetResponse{}). Returns(400, "create failure", bcode.Bcode{}). Writes(apis.DetailTargetResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}").To(dt.detailTarget). + ws.Route(ws.GET("/{targetName}").To(dt.detailTarget). Doc("detail Target"). - Param(ws.PathParameter("name", "identifier of the Target.").DataType("string")). + Param(ws.PathParameter("targetName", "identifier of the Target.").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(dt.targetCheckFilter). + Filter(dt.rbacUsecase.CheckPerm("target", "detail")). Returns(200, "create success", apis.DetailTargetResponse{}). Writes(apis.DetailTargetResponse{}).Do(returns200, returns500)) - ws.Route(ws.PUT("/{name}").To(dt.updateTarget). + ws.Route(ws.PUT("/{targetName}").To(dt.updateTarget). Doc("update application Target config"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(dt.targetCheckFilter). - Param(ws.PathParameter("name", "identifier of the Target").DataType("string")). + Param(ws.PathParameter("targetName", "identifier of the Target").DataType("string")). Reads(apis.UpdateTargetRequest{}). + Filter(dt.rbacUsecase.CheckPerm("target", "update")). Returns(200, "OK", apis.DetailTargetResponse{}). Writes(apis.DetailTargetResponse{}).Do(returns200, returns500)) - ws.Route(ws.DELETE("/{name}").To(dt.deleteTarget). + ws.Route(ws.DELETE("/{targetName}").To(dt.deleteTarget). Doc("deletet Target"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(dt.targetCheckFilter). - Param(ws.PathParameter("name", "identifier of the Target").DataType("string")). + Filter(dt.rbacUsecase.CheckPerm("target", "delete")). + Param(ws.PathParameter("targetName", "identifier of the Target").DataType("string")). Returns(200, "OK", apis.EmptyResponse{}). Writes(apis.EmptyResponse{}).Do(returns200, returns500)) @@ -129,7 +137,7 @@ func (dt *TargetWebService) createTarget(req *restful.Request, res *restful.Resp } func (dt *TargetWebService) targetCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - Target, err := dt.TargetUsecase.GetTarget(req.Request.Context(), req.PathParameter("name")) + Target, err := dt.TargetUsecase.GetTarget(req.Request.Context(), req.PathParameter("targetName")) if err != nil { bcode.ReturnError(req, res, err) return @@ -175,7 +183,7 @@ func (dt *TargetWebService) updateTarget(req *restful.Request, res *restful.Resp } func (dt *TargetWebService) deleteTarget(req *restful.Request, res *restful.Response) { - TargetName := req.PathParameter("name") + TargetName := req.PathParameter("targetName") // Target in use, can't be deleted applications, err := dt.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicationOptions{TargetName: TargetName}) if err != nil { @@ -204,7 +212,7 @@ func (dt *TargetWebService) listTargets(req *restful.Request, res *restful.Respo bcode.ReturnError(req, res, err) return } - Targets, err := dt.TargetUsecase.ListTargets(req.Request.Context(), page, pageSize) + Targets, err := dt.TargetUsecase.ListTargets(req.Request.Context(), page, pageSize, req.QueryParameter("project")) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/user.go b/pkg/apiserver/rest/webservice/user.go index c7c1f7b95..0687f6586 100644 --- a/pkg/apiserver/rest/webservice/user.go +++ b/pkg/apiserver/rest/webservice/user.go @@ -31,12 +31,14 @@ import ( type userWebService struct { userUsecase usecase.UserUsecase + rbacUsecase usecase.RBACUsecase } // NewUserWebService is the webservice of user -func NewUserWebService(userUsecase usecase.UserUsecase) WebService { +func NewUserWebService(userUsecase usecase.UserUsecase, rbacUsecase usecase.RBACUsecase) WebService { return &userWebService{ userUsecase: userUsecase, + rbacUsecase: rbacUsecase, } } @@ -52,6 +54,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(c.listUser). Doc("list users"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("user", "list")). Param(ws.QueryParameter("page", "query the page number").DataType("integer")). Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). Param(ws.QueryParameter("name", "fuzzy search based on name").DataType("string")). @@ -63,6 +66,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/").To(c.createUser). Doc("create a user"). + Filter(c.rbacUsecase.CheckPerm("user", "create")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateUserRequest{}). Returns(200, "OK", apis.UserBase{}). @@ -72,6 +76,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{username}").To(c.detailUser). Doc("get user detail"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("user", "detail")). Filter(c.userCheckFilter). Returns(200, "OK", apis.DetailUserResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). @@ -80,6 +85,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.PUT("/{username}").To(c.updateUser). Doc("update a user's alias or password"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("user", "update")). Filter(c.userCheckFilter). Returns(200, "OK", apis.UserBase{}). Returns(400, "Bad Request", bcode.Bcode{}). @@ -88,6 +94,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.DELETE("/{username}").To(c.deleteUser). Doc("delete a user"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("user", "delete")). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). Writes(apis.EmptyResponse{})) @@ -95,6 +102,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{username}/disable").To(c.disableUser). Doc("disable a user"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("user", "disable")). Filter(c.userCheckFilter). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). @@ -103,6 +111,7 @@ func (c *userWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{username}/enable").To(c.enableUser). Doc("enable a user"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.rbacUsecase.CheckPerm("user", "enable")). Filter(c.userCheckFilter). Returns(200, "OK", apis.EmptyResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). diff --git a/pkg/apiserver/rest/webservice/velaql.go b/pkg/apiserver/rest/webservice/velaql.go index 21018663a..77d01a1a4 100644 --- a/pkg/apiserver/rest/webservice/velaql.go +++ b/pkg/apiserver/rest/webservice/velaql.go @@ -27,12 +27,14 @@ import ( type velaQLWebService struct { velaQLUsecase usecase.VelaQLUsecase + rbacUsecase usecase.RBACUsecase } // NewVelaQLWebService new velaQL webservice -func NewVelaQLWebService(velaQLUsecase usecase.VelaQLUsecase) WebService { +func NewVelaQLWebService(velaQLUsecase usecase.VelaQLUsecase, rbacUsecase usecase.RBACUsecase) WebService { return &velaQLWebService{ velaQLUsecase: velaQLUsecase, + rbacUsecase: rbacUsecase, } } @@ -48,6 +50,8 @@ func (v *velaQLWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(v.queryView). Doc("use velaQL to query resource status"). Metadata(restfulspec.KeyOpenAPITags, tags). + // TODO: VelaQL is an open data query API that is currently not compatible with RBAC. + // Filter(v.rbacUsecase.CheckPerm("application", "detail")). Param(ws.QueryParameter("velaql", "velaql query statement").DataType("string")). Returns(200, "OK", apis.VelaQLViewResponse{}). Returns(400, "Bad Request", bcode.Bcode{}). diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 85d8ca970..9f0d90813 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -17,6 +17,8 @@ limitations under the License. package webservice import ( + "context" + "log" "net/http" "time" @@ -37,7 +39,7 @@ type WebService interface { var registeredWebService []WebService -// RegisterWebService regist webservice +// RegisterWebService register webservice func RegisterWebService(ws WebService) { registeredWebService = append(registeredWebService, ws) } @@ -59,12 +61,13 @@ func returns500(b *restful.RouteBuilder) { // Init init all webservice, pass in the required parameter object. // It can be implemented using the idea of dependency injection. -func Init(ds datastore.DataStore, addonCacheTime time.Duration) { +func Init(ctx context.Context, ds datastore.DataStore, addonCacheTime time.Duration, initDatabase bool) map[string]interface{} { clusterUsecase := usecase.NewClusterUsecase(ds) - envUsecase := usecase.NewEnvUsecase(ds) - workflowUsecase := usecase.NewWorkflowUsecase(ds, envUsecase) - projectUsecase := usecase.NewProjectUsecase(ds) + rbacUsecase := usecase.NewRBACUsecase(ds) + projectUsecase := usecase.NewProjectUsecase(ds, rbacUsecase) + envUsecase := usecase.NewEnvUsecase(ds, projectUsecase) targetUsecase := usecase.NewTargetUsecase(ds) + workflowUsecase := usecase.NewWorkflowUsecase(ds, envUsecase) oamApplicationUsecase := usecase.NewOAMApplicationUsecase() velaQLUsecase := usecase.NewVelaQLUsecase() definitionUsecase := usecase.NewDefinitionUsecase() @@ -74,36 +77,55 @@ func Init(ds datastore.DataStore, addonCacheTime time.Duration) { webhookUsecase := usecase.NewWebhookUsecase(ds, applicationUsecase) systemInfoUsecase := usecase.NewSystemInfoUsecase(ds) helmUsecase := usecase.NewHelmUsecase() - userUsecase := usecase.NewUserUsecase(ds, projectUsecase, systemInfoUsecase) + userUsecase := usecase.NewUserUsecase(ds, projectUsecase, systemInfoUsecase, rbacUsecase) authenticationUsecase := usecase.NewAuthenticationUsecase(ds, systemInfoUsecase, userUsecase) - - // init for default values + // Modules that require default data initialization, Call it here in order + if initDatabase { + initData(ctx, userUsecase, rbacUsecase, projectUsecase, targetUsecase) + } // Application - RegisterWebService(NewApplicationWebService(applicationUsecase, envBindingUsecase, workflowUsecase)) - RegisterWebService(NewProjectWebService(projectUsecase)) - RegisterWebService(NewEnvWebService(envUsecase, applicationUsecase)) + RegisterWebService(NewApplicationWebService(applicationUsecase, envBindingUsecase, workflowUsecase, rbacUsecase)) + RegisterWebService(NewProjectWebService(projectUsecase, rbacUsecase, targetUsecase)) + RegisterWebService(NewEnvWebService(envUsecase, applicationUsecase, rbacUsecase)) // Extension - RegisterWebService(NewDefinitionWebservice(definitionUsecase)) - RegisterWebService(NewAddonWebService(addonUsecase)) - RegisterWebService(NewEnabledAddonWebService(addonUsecase)) - RegisterWebService(NewAddonRegistryWebService(addonUsecase)) + RegisterWebService(NewDefinitionWebservice(definitionUsecase, rbacUsecase)) + RegisterWebService(NewAddonWebService(addonUsecase, rbacUsecase)) + RegisterWebService(NewEnabledAddonWebService(addonUsecase, rbacUsecase)) + RegisterWebService(NewAddonRegistryWebService(addonUsecase, rbacUsecase)) // Resources - RegisterWebService(NewClusterWebService(clusterUsecase)) - RegisterWebService(NewOAMApplication(oamApplicationUsecase)) + RegisterWebService(NewClusterWebService(clusterUsecase, rbacUsecase)) + RegisterWebService(NewOAMApplication(oamApplicationUsecase, rbacUsecase)) RegisterWebService(&policyDefinitionWebservice{}) RegisterWebService(&payloadTypesWebservice{}) - RegisterWebService(NewTargetWebService(targetUsecase, applicationUsecase)) - RegisterWebService(NewVelaQLWebService(velaQLUsecase)) + RegisterWebService(NewTargetWebService(targetUsecase, applicationUsecase, rbacUsecase)) + RegisterWebService(NewVelaQLWebService(velaQLUsecase, rbacUsecase)) RegisterWebService(NewWebhookWebService(webhookUsecase, applicationUsecase)) + RegisterWebService(NewHelmWebService(helmUsecase)) // Authentication - RegisterWebService(NewAuthenticationWebService(authenticationUsecase)) - RegisterWebService(NewUserWebService(userUsecase)) + RegisterWebService(NewAuthenticationWebService(authenticationUsecase, userUsecase)) + RegisterWebService(NewUserWebService(userUsecase, rbacUsecase)) + RegisterWebService(NewSystemInfoWebService(systemInfoUsecase, rbacUsecase)) - RegisterWebService(NewSystemInfoWebService(systemInfoUsecase)) + // RBAC + RegisterWebService(NewRBACWebService(rbacUsecase)) - RegisterWebService(NewHelmWebService(helmUsecase)) + // return some usecase instance + return map[string]interface{}{"workflow": workflowUsecase, "project": projectUsecase} +} + +// InitUsecase the usecase set that needs init data +type InitUsecase interface { + Init(ctx context.Context) error +} + +func initData(ctx context.Context, inits ...InitUsecase) { + for _, init := range inits { + if err := init.Init(ctx); err != nil { + log.Fatalf("database init failure %s", err.Error()) + } + } } diff --git a/pkg/apiserver/sync/convert.go b/pkg/apiserver/sync/convert.go index 429e0635f..ade0ccd03 100644 --- a/pkg/apiserver/sync/convert.go +++ b/pkg/apiserver/sync/convert.go @@ -1,17 +1,17 @@ /* - Copyright 2022 The KubeVela Authors. +Copyright 2022 The KubeVela Authors. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package sync @@ -20,138 +20,14 @@ import ( "context" "strconv" "strings" - "time" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/apiserver/model" - "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/apiserver/sync/convert" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/workflow/step" ) -// ConvertFromCRComponent concerts Application CR Component object into velaux data store component -func ConvertFromCRComponent(appPrimaryKey string, component common.ApplicationComponent) (model.ApplicationComponent, error) { - bc := model.ApplicationComponent{ - AppPrimaryKey: appPrimaryKey, - Name: component.Name, - Type: component.Type, - ExternalRevision: component.ExternalRevision, - DependsOn: component.DependsOn, - Inputs: component.Inputs, - Outputs: component.Outputs, - Scopes: component.Scopes, - Creator: model.AutoGenComp, - } - if component.Properties != nil { - properties, err := model.NewJSONStruct(component.Properties) - if err != nil { - return bc, err - } - bc.Properties = properties - } - for _, trait := range component.Traits { - properties, err := model.NewJSONStruct(trait.Properties) - if err != nil { - return bc, err - } - bc.Traits = append(bc.Traits, model.ApplicationTrait{CreateTime: time.Now(), UpdateTime: time.Now(), Properties: properties, Type: trait.Type, Alias: trait.Type, Description: "auto gen"}) - } - return bc, nil -} - -// ConvertFromCRPolicy converts Application CR Policy object into velaux data store policy -func ConvertFromCRPolicy(appPrimaryKey string, policyCR v1beta1.AppPolicy, creator string) (model.ApplicationPolicy, error) { - plc := model.ApplicationPolicy{ - AppPrimaryKey: appPrimaryKey, - Name: policyCR.Name, - Type: policyCR.Type, - Creator: creator, - } - if policyCR.Properties != nil { - properties, err := model.NewJSONStruct(policyCR.Properties) - if err != nil { - return plc, err - } - plc.Properties = properties - } - return plc, nil -} - -// ConvertFromCRWorkflow converts Application CR Workflow section into velaux data store workflow -func ConvertFromCRWorkflow(ctx context.Context, cli client.Client, appPrimaryKey string, app *v1beta1.Application) (model.Workflow, []v1beta1.WorkflowStep, error) { - dataWf := model.Workflow{ - AppPrimaryKey: appPrimaryKey, - // every namespace has a synced env - EnvName: model.AutoGenEnvNamePrefix + app.Namespace, - // every application has a synced workflow - Name: model.AutoGenWorkflowNamePrefix + appPrimaryKey, - Alias: model.AutoGenWorkflowNamePrefix + app.Name, - Description: model.AutoGenDesc, - } - if app.Spec.Workflow == nil { - return dataWf, nil, nil - } - var steps []v1beta1.WorkflowStep - if app.Spec.Workflow.Ref != "" { - dataWf.Name = app.Spec.Workflow.Ref - wf := &v1alpha1.Workflow{} - if err := cli.Get(ctx, types.NamespacedName{Namespace: app.GetNamespace(), Name: app.Spec.Workflow.Ref}, wf); err != nil { - return dataWf, nil, err - } - steps = step.ConvertSteps(wf.Steps) - } else { - steps = app.Spec.Workflow.Steps - } - for _, s := range steps { - if s.Properties == nil { - continue - } - properties, err := model.NewJSONStruct(s.Properties) - if err != nil { - return dataWf, nil, err - } - dataWf.Steps = append(dataWf.Steps, model.WorkflowStep{ - Name: s.Name, - Type: s.Type, - Inputs: s.Inputs, - Outputs: s.Outputs, - DependsOn: s.DependsOn, - Properties: properties, - }) - } - return dataWf, steps, nil -} - -// ConvertFromCRTargets converts deployed Cluster/Namespace from Application CR Status into velaux data store -func ConvertFromCRTargets(targetApp *v1beta1.Application) []*model.Target { - var targets []*model.Target - nc := make(map[string]struct{}) - for _, v := range targetApp.Status.AppliedResources { - var cluster = v.Cluster - if cluster == "" { - cluster = multicluster.ClusterLocalName - } - name := model.AutoGenTargetNamePrefix + cluster + "-" + v.Namespace - if _, ok := nc[name]; ok { - continue - } - nc[name] = struct{}{} - targets = append(targets, &model.Target{ - Name: name, - Cluster: &model.ClusterTarget{ - ClusterName: cluster, - Namespace: v.Namespace, - }, - }) - } - return targets -} - // ConvertApp2DatastoreApp will convert Application CR to datastore application related resources func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1.Application) (*model.DataStoreApp, error) { cli := c.cli @@ -193,7 +69,7 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1. // 2. convert component and trait for _, cmp := range targetApp.Spec.Components { - compModel, err := ConvertFromCRComponent(appMeta.PrimaryKey(), cmp) + compModel, err := convert.FromCRComponent(appMeta.PrimaryKey(), cmp) if err != nil { return nil, err } @@ -201,7 +77,7 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1. } // 3. convert workflow - wf, steps, err := ConvertFromCRWorkflow(ctx, cli, appMeta.PrimaryKey(), targetApp) + wf, steps, err := convert.FromCRWorkflow(ctx, cli, appMeta.PrimaryKey(), targetApp) if err != nil { return nil, err } @@ -217,7 +93,7 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1. return nil, err } for _, plc := range outsidePLC { - plcModel, err := ConvertFromCRPolicy(appMeta.PrimaryKey(), plc, model.AutoGenRefPolicy) + plcModel, err := convert.FromCRPolicy(appMeta.PrimaryKey(), plc, model.AutoGenRefPolicy) if _, ok := innerPlc[plc.Name]; ok { plcModel.Creator = model.AutoGenPolicy } diff --git a/pkg/apiserver/sync/convert/convert.go b/pkg/apiserver/sync/convert/convert.go new file mode 100644 index 000000000..29e877b63 --- /dev/null +++ b/pkg/apiserver/sync/convert/convert.go @@ -0,0 +1,150 @@ +/* + Copyright 2022 The KubeVela Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package convert + +import ( + "context" + "time" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/workflow/step" +) + +// FromCRComponent concerts Application CR Component object into velaux data store component +func FromCRComponent(appPrimaryKey string, component common.ApplicationComponent) (model.ApplicationComponent, error) { + bc := model.ApplicationComponent{ + AppPrimaryKey: appPrimaryKey, + Name: component.Name, + Type: component.Type, + ExternalRevision: component.ExternalRevision, + DependsOn: component.DependsOn, + Inputs: component.Inputs, + Outputs: component.Outputs, + Scopes: component.Scopes, + Creator: model.AutoGenComp, + } + if component.Properties != nil { + properties, err := model.NewJSONStruct(component.Properties) + if err != nil { + return bc, err + } + bc.Properties = properties + } + for _, trait := range component.Traits { + properties, err := model.NewJSONStruct(trait.Properties) + if err != nil { + return bc, err + } + bc.Traits = append(bc.Traits, model.ApplicationTrait{CreateTime: time.Now(), UpdateTime: time.Now(), Properties: properties, Type: trait.Type, Alias: trait.Type, Description: "auto gen"}) + } + return bc, nil +} + +// FromCRPolicy converts Application CR Policy object into velaux data store policy +func FromCRPolicy(appPrimaryKey string, policyCR v1beta1.AppPolicy, creator string) (model.ApplicationPolicy, error) { + plc := model.ApplicationPolicy{ + AppPrimaryKey: appPrimaryKey, + Name: policyCR.Name, + Type: policyCR.Type, + Creator: creator, + } + if policyCR.Properties != nil { + properties, err := model.NewJSONStruct(policyCR.Properties) + if err != nil { + return plc, err + } + plc.Properties = properties + } + return plc, nil +} + +// FromCRWorkflow converts Application CR Workflow section into velaux data store workflow +func FromCRWorkflow(ctx context.Context, cli client.Client, appPrimaryKey string, app *v1beta1.Application) (model.Workflow, []v1beta1.WorkflowStep, error) { + dataWf := model.Workflow{ + AppPrimaryKey: appPrimaryKey, + // every namespace has a synced env + EnvName: model.AutoGenEnvNamePrefix + app.Namespace, + // every application has a synced workflow + Name: model.AutoGenWorkflowNamePrefix + appPrimaryKey, + Alias: model.AutoGenWorkflowNamePrefix + app.Name, + Description: model.AutoGenDesc, + } + if app.Spec.Workflow == nil { + return dataWf, nil, nil + } + var steps []v1beta1.WorkflowStep + if app.Spec.Workflow.Ref != "" { + dataWf.Name = app.Spec.Workflow.Ref + wf := &v1alpha1.Workflow{} + if err := cli.Get(ctx, types.NamespacedName{Namespace: app.GetNamespace(), Name: app.Spec.Workflow.Ref}, wf); err != nil { + return dataWf, nil, err + } + steps = step.ConvertSteps(wf.Steps) + } else { + steps = app.Spec.Workflow.Steps + } + for _, s := range steps { + if s.Properties == nil { + continue + } + properties, err := model.NewJSONStruct(s.Properties) + if err != nil { + return dataWf, nil, err + } + dataWf.Steps = append(dataWf.Steps, model.WorkflowStep{ + Name: s.Name, + Type: s.Type, + Inputs: s.Inputs, + Outputs: s.Outputs, + DependsOn: s.DependsOn, + Properties: properties, + }) + } + return dataWf, steps, nil +} + +// FromCRTargets converts deployed Cluster/Namespace from Application CR Status into velaux data store +func FromCRTargets(targetApp *v1beta1.Application) []*model.Target { + var targets []*model.Target + nc := make(map[string]struct{}) + for _, v := range targetApp.Status.AppliedResources { + var cluster = v.Cluster + if cluster == "" { + cluster = multicluster.ClusterLocalName + } + name := model.AutoGenTargetNamePrefix + cluster + "-" + v.Namespace + if _, ok := nc[name]; ok { + continue + } + nc[name] = struct{}{} + targets = append(targets, &model.Target{ + Name: name, + Cluster: &model.ClusterTarget{ + ClusterName: cluster, + Namespace: v.Namespace, + }, + }) + } + return targets +} diff --git a/pkg/apiserver/sync/cr2ux.go b/pkg/apiserver/sync/cr2ux.go index cb014fba3..14f962a93 100644 --- a/pkg/apiserver/sync/cr2ux.go +++ b/pkg/apiserver/sync/cr2ux.go @@ -26,6 +26,7 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/oam" ) @@ -79,9 +80,10 @@ func (c *CR2UX) getApp(ctx context.Context, name, namespace string) (*model.Appl // CR2UX provides the Add/Update/Delete method type CR2UX struct { - ds datastore.DataStore - cli client.Client - cache sync.Map + ds datastore.DataStore + cli client.Client + cache sync.Map + usecases map[string]interface{} } func formatAppComposedName(name, namespace string) string { @@ -106,8 +108,11 @@ func (c *CR2UX) AddOrUpdate(ctx context.Context, targetApp *v1beta1.Application) log.Logger.Errorf("Convert App to data store err %v", err) return err } - - if err = StoreProject(ctx, dsApp.AppMeta.Project, ds); err != nil { + pu, ok := c.usecases["project"].(usecase.ProjectUsecase) + if !ok { + log.Logger.Warnf("not provide project usecase instance") + } + if err = StoreProject(ctx, dsApp.AppMeta.Project, ds, pu); err != nil { log.Logger.Errorf("get or create project for sync process err %v", err) return err } diff --git a/pkg/apiserver/sync/store.go b/pkg/apiserver/sync/store.go index f9425ebcc..0f740ed28 100644 --- a/pkg/apiserver/sync/store.go +++ b/pkg/apiserver/sync/store.go @@ -24,11 +24,13 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/utils" ) // StoreProject will create project for synced application -func StoreProject(ctx context.Context, name string, ds datastore.DataStore) error { +func StoreProject(ctx context.Context, name string, ds datastore.DataStore, projectUsecase usecase.ProjectUsecase) error { err := ds.Get(ctx, &model.Project{Name: name}) if err == nil { // it means the record already exists, don't need to add anything @@ -38,12 +40,15 @@ func StoreProject(ctx context.Context, name string, ds datastore.DataStore) erro // other database error, return it return err } - proj := &model.Project{ - Name: name, - Description: model.AutoGenProj, - Alias: strings.Title(name), + if projectUsecase != nil { + _, err := projectUsecase.CreateProject(ctx, v1.CreateProjectRequest{ + Name: name, + Alias: strings.Title(name), + Owner: model.DefaultAdminUserName, + Description: model.AutoGenProj}) + return err } - return ds.Add(ctx, proj) + return nil } // StoreAppMeta will sync application metadata from CR to datastore diff --git a/pkg/apiserver/sync/worker.go b/pkg/apiserver/sync/worker.go index bc5708650..295e9aefc 100644 --- a/pkg/apiserver/sync/worker.go +++ b/pkg/apiserver/sync/worker.go @@ -37,7 +37,7 @@ import ( ) // Start prepares watchers and run their controllers, then waits for process termination signals -func Start(ctx context.Context, ds datastore.DataStore, cfg *rest.Config) { +func Start(ctx context.Context, ds datastore.DataStore, cfg *rest.Config, usecases map[string]interface{}) { k8sClient, err := clients.GetKubeClient() if err != nil { logrus.Fatal(err) @@ -50,10 +50,10 @@ func Start(ctx context.Context, ds datastore.DataStore, cfg *rest.Config) { f := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, 0, v1.NamespaceAll, nil) - startAppSyncing(ctx, f, ds, k8sClient) + startAppSyncing(ctx, f, ds, k8sClient, usecases) } -func startAppSyncing(ctx context.Context, factory dynamicinformer.DynamicSharedInformerFactory, ds datastore.DataStore, cli client.Client) { +func startAppSyncing(ctx context.Context, factory dynamicinformer.DynamicSharedInformerFactory, ds datastore.DataStore, cli client.Client, usecases map[string]interface{}) { var err error informer := factory.ForResource(v1beta1.SchemeGroupVersion.WithResource("applications")).Informer() getApp := func(obj interface{}) *v1beta1.Application { @@ -62,10 +62,14 @@ func startAppSyncing(ctx context.Context, factory dynamicinformer.DynamicSharedI _ = json.Unmarshal(bs, app) return app } + if usecases == nil { + usecases = make(map[string]interface{}) + } cu := &CR2UX{ - ds: ds, - cli: cli, - cache: sync.Map{}, + ds: ds, + cli: cli, + cache: sync.Map{}, + usecases: usecases, } if err = cu.initCache(ctx); err != nil { klog.Fatal("sync app init err", err) diff --git a/pkg/apiserver/sync/worker_test.go b/pkg/apiserver/sync/worker_test.go index f13911ee6..8be05cf79 100644 --- a/pkg/apiserver/sync/worker_test.go +++ b/pkg/apiserver/sync/worker_test.go @@ -61,7 +61,7 @@ var _ = Describe("Test Worker CR sync to datastore", func() { By("Start syncing") ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go Start(ctx, ds, cfg) + go Start(ctx, ds, cfg, nil) By("create test app1 and check the syncing results") app1 := &v1beta1.Application{} diff --git a/pkg/utils/strings.go b/pkg/utils/strings.go index 77678c54e..aa1e03519 100644 --- a/pkg/utils/strings.go +++ b/pkg/utils/strings.go @@ -63,3 +63,25 @@ func EqualSlice(a, b []string) bool { sort.Strings(b) return reflect.DeepEqual(a, b) } + +// SliceIncludeSlice the a slice include the b slice +func SliceIncludeSlice(a, b []string) bool { + if EqualSlice(a, b) { + return true + } + for _, item := range b { + if !StringsContain(a, item) { + return false + } + } + return true +} + +// MapKey2Array convery map keys to array +func MapKey2Array(source map[string]string) []string { + var list []string + for k := range source { + list = append(list, k) + } + return list +} diff --git a/pkg/utils/strings_test.go b/pkg/utils/strings_test.go index 7ad35faf8..aa96935da 100644 --- a/pkg/utils/strings_test.go +++ b/pkg/utils/strings_test.go @@ -77,3 +77,25 @@ func TestEqual(t *testing.T) { caseB = []string{"b", "a", "c"} assert.Equal(t, EqualSlice(caseA, caseB), false) } + +func TestSliceIncludeSlice(t *testing.T) { + caseA := []string{"b", "a", "c"} + caseB := []string{} + assert.Equal(t, SliceIncludeSlice(caseA, caseB), true) + + caseA = []string{"b", "a", "c"} + caseB = []string{"b"} + assert.Equal(t, SliceIncludeSlice(caseA, caseB), true) + + caseA = []string{"b", "a", "c"} + caseB = []string{"b", "c"} + assert.Equal(t, SliceIncludeSlice(caseA, caseB), true) + + caseA = []string{"b", "a", "c"} + caseB = []string{"b", "c", "d"} + assert.Equal(t, SliceIncludeSlice(caseA, caseB), false) + + caseA = []string{"b", "a", "c"} + caseB = []string{"b", "c", "a"} + assert.Equal(t, SliceIncludeSlice(caseA, caseB), true) +} diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 6961dc4b3..91ebcb791 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -80,7 +80,8 @@ var _ = Describe("Test application rest api", func() { var envName = "dev" // create target var createTarget = apisv1.CreateTargetRequest{ - Name: targetName, + Name: targetName, + Project: appProject, Cluster: &apisv1.ClusterTarget{ ClusterName: "local", Namespace: targetName, diff --git a/test/e2e-apiserver-test/env_test.go b/test/e2e-apiserver-test/env_test.go index 423c28691..6e75242c4 100644 --- a/test/e2e-apiserver-test/env_test.go +++ b/test/e2e-apiserver-test/env_test.go @@ -40,16 +40,24 @@ var _ = Describe("Test env rest api", func() { It("Test create, get, delete env with normal format", func() { defer GinkgoRecover() + var re = apisv1.CreateProjectRequest{ + Name: "my-pro", + Alias: "project alias", + } + var proBase apisv1.ProjectBase + resp := post("/projects", re) + Expect(decodeResponseBody(resp, &proBase)).Should(Succeed()) By("create a target for preparation") var reqt = apisv1.CreateTargetRequest{ Name: testtarget1, Alias: "my-target-for-env1", Description: "KubeVela Target", + Project: "my-pro", Cluster: &apisv1.ClusterTarget{ClusterName: multicluster.ClusterLocalName, Namespace: testtarget1}, } var tgBase apisv1.TargetBase - resp := post("/targets", reqt) + resp = post("/targets", reqt) Expect(decodeResponseBody(resp, &tgBase)).Should(Succeed()) By("create the first env") @@ -100,6 +108,7 @@ var _ = Describe("Test env rest api", func() { Name: testtarget1, Alias: "my-target-for-env2", Description: "KubeVela Target", + Project: "my-pro", Cluster: &apisv1.ClusterTarget{ClusterName: multicluster.ClusterLocalName, Namespace: testtarget1}, } var tgBase apisv1.TargetBase @@ -109,6 +118,7 @@ var _ = Describe("Test env rest api", func() { Name: testtarget2, Alias: "my-target-for-env3", Description: "KubeVela Target", + Project: "my-pro", Cluster: &apisv1.ClusterTarget{ClusterName: multicluster.ClusterLocalName, Namespace: testtarget2}, } resp = post("/targets", reqt) diff --git a/test/e2e-apiserver-test/oam_application_test.go b/test/e2e-apiserver-test/oam_application_test.go index 06f11cf08..5420946a0 100644 --- a/test/e2e-apiserver-test/oam_application_test.go +++ b/test/e2e-apiserver-test/oam_application_test.go @@ -17,11 +17,9 @@ package e2e_apiserver_test import ( - "bytes" "context" "encoding/json" "fmt" - "net/http" "time" "github.com/google/go-cmp/cmp" @@ -49,14 +47,7 @@ var _ = Describe("Test oam application rest api", func() { Policies: app.Spec.Policies, Workflow: app.Spec.Workflow, } - bodyByte, err := json.Marshal(req) - Expect(err).Should(BeNil()) - res, err := http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), - "application/json", - bytes.NewBuffer(bodyByte), - ) - Expect(err).ShouldNot(HaveOccurred()) + res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appName), req) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) Expect(res.Body).ShouldNot(BeNil()) @@ -73,15 +64,8 @@ var _ = Describe("Test oam application rest api", func() { updateReq := apiv1.ApplicationRequest{ Components: app.Spec.Components[1:], } - bodyByte, err = json.Marshal(updateReq) - Expect(err).Should(BeNil()) Eventually(func(g Gomega) { - res, err = http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), - "application/json", - bytes.NewBuffer(bodyByte), - ) - g.Expect(err).ShouldNot(HaveOccurred()) + res = post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appName), updateReq) g.Expect(res).ShouldNot(BeNil()) g.Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) g.Expect(res.Body).ShouldNot(BeNil()) @@ -96,15 +80,12 @@ var _ = Describe("Test oam application rest api", func() { It("Test get oam app", func() { defer GinkgoRecover() - res, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), - ) - Expect(err).ShouldNot(HaveOccurred()) + res := get(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appName)) Expect(res).ShouldNot(BeNil()) defer res.Body.Close() var appResp apiv1.ApplicationResponse - err = json.NewDecoder(res.Body).Decode(&appResp) + err := json.NewDecoder(res.Body).Decode(&appResp) Expect(err).ShouldNot(HaveOccurred()) Expect(len(appResp.Spec.Components)).Should(Equal(1)) @@ -112,10 +93,7 @@ var _ = Describe("Test oam application rest api", func() { It("Test delete oam app", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), nil) - Expect(err).ShouldNot(HaveOccurred()) - res, err := http.DefaultClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) + res := delete(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appName)) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) }) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index 21f670cdd..d0b26d9a6 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -20,7 +20,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "net/http" "strings" @@ -38,12 +37,14 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/datastore" arest "github.com/oam-dev/kubevela/pkg/apiserver/rest" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) var k8sClient client.Client var token string const ( + baseDomain = "http://127.0.0.1:8000" baseURL = "http://127.0.0.1:8000/api/v1" testNSprefix = "api-e2e-test-" ) @@ -101,11 +102,11 @@ var _ = BeforeSuite(func() { if err != nil { return err } - loginResp := &apisv1.LoginResponse{} - err = json.NewDecoder(resp.Body).Decode(loginResp) - Expect(err).Should(BeNil()) - token = "Bearer " + loginResp.AccessToken - if resp.StatusCode == http.StatusOK { + if resp.StatusCode == 200 { + loginResp := &apisv1.LoginResponse{} + err = json.NewDecoder(resp.Body).Decode(loginResp) + Expect(err).Should(BeNil()) + token = "Bearer " + loginResp.AccessToken var req = apisv1.CreateProjectRequest{ Name: appProject, Description: "test project", @@ -113,8 +114,11 @@ var _ = BeforeSuite(func() { _ = post("/projects", req) return nil } - return errors.New("rest service not ready") - }, time.Second*5, time.Millisecond*200).Should(BeNil()) + code := &bcode.Bcode{} + err = json.NewDecoder(resp.Body).Decode(code) + Expect(err).Should(BeNil()) + return fmt.Errorf("rest service not ready code:%d message:%s", resp.StatusCode, code.Message) + }, time.Second*10, time.Millisecond*200).Should(BeNil()) By("api server started") }) @@ -134,7 +138,12 @@ func post(path string, body interface{}) *http.Response { b, err := json.Marshal(body) Expect(err).Should(BeNil()) client := &http.Client{} - req, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewBuffer(b)) + if !strings.HasPrefix(path, "/v1") { + path = baseURL + path + } else { + path = baseDomain + path + } + req, err := http.NewRequest(http.MethodPost, path, bytes.NewBuffer(b)) Expect(err).Should(BeNil()) req.Header.Add("Authorization", token) req.Header.Add("Content-Type", "application/json") @@ -148,7 +157,12 @@ func put(path string, body interface{}) *http.Response { b, err := json.Marshal(body) Expect(err).Should(BeNil()) client := &http.Client{} - req, err := http.NewRequest(http.MethodPut, baseURL+path, bytes.NewBuffer(b)) + if !strings.HasPrefix(path, "/v1") { + path = baseURL + path + } else { + path = baseDomain + path + } + req, err := http.NewRequest(http.MethodPut, path, bytes.NewBuffer(b)) Expect(err).Should(BeNil()) req.Header.Add("Authorization", token) req.Header.Set("Content-Type", "application/json") @@ -160,7 +174,12 @@ func put(path string, body interface{}) *http.Response { func get(path string) *http.Response { client := &http.Client{} - req, err := http.NewRequest(http.MethodGet, baseURL+path, nil) + if !strings.HasPrefix(path, "/v1") { + path = baseURL + path + } else { + path = baseDomain + path + } + req, err := http.NewRequest(http.MethodGet, path, nil) Expect(err).Should(BeNil()) req.Header.Add("Authorization", token) @@ -171,10 +190,14 @@ func get(path string) *http.Response { func delete(path string) *http.Response { client := &http.Client{} - req, err := http.NewRequest(http.MethodDelete, baseURL+path, nil) + if !strings.HasPrefix(path, "/v1") { + path = baseURL + path + } else { + path = baseDomain + path + } + req, err := http.NewRequest(http.MethodDelete, path, nil) Expect(err).Should(BeNil()) req.Header.Add("Authorization", token) - response, err := client.Do(req) Expect(err).Should(BeNil()) defer response.Body.Close() diff --git a/test/e2e-apiserver-test/system_info_test.go b/test/e2e-apiserver-test/system_info_test.go index abc836972..763674c3b 100644 --- a/test/e2e-apiserver-test/system_info_test.go +++ b/test/e2e-apiserver-test/system_info_test.go @@ -24,10 +24,6 @@ import ( ) var _ = Describe("Test system info rest api", func() { - BeforeEach(func() { - res := delete("/system_info/") - Expect(decodeResponseBody(res, nil)).Should(Succeed()) - }) It("Test get SystemInfo", func() { res := get("/system_info/") diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go index 3c267e49b..3bf630341 100644 --- a/test/e2e-apiserver-test/velaql_test.go +++ b/test/e2e-apiserver-test/velaql_test.go @@ -17,11 +17,9 @@ package e2e_apiserver_test import ( - "bytes" "context" "encoding/json" "fmt" - "net/http" "time" . "github.com/onsi/ginkgo" @@ -70,14 +68,7 @@ var _ = Describe("Test velaQL rest api", func() { req := apiv1.ApplicationRequest{ Components: app.Spec.Components, } - bodyByte, err := json.Marshal(req) - Expect(err).Should(BeNil()) - res, err := http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), - "application/json", - bytes.NewBuffer(bodyByte), - ) - Expect(err).ShouldNot(HaveOccurred()) + res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appName), req) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) @@ -218,14 +209,7 @@ var _ = Describe("Test velaQL rest api", func() { req := apiv1.ApplicationRequest{ Components: appWithHelm.Spec.Components, } - bodyByte, err := json.Marshal(req) - Expect(err).Should(BeNil()) - res, err := http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), - "application/json", - bytes.NewBuffer(bodyByte), - ) - Expect(err).ShouldNot(HaveOccurred()) + res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), req) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) @@ -248,7 +232,7 @@ var _ = Describe("Test velaQL rest api", func() { defer queryRes.Body.Close() status := new(Status) - err = json.NewDecoder(queryRes.Body).Decode(status) + err := json.NewDecoder(queryRes.Body).Decode(status) if err != nil { return err } @@ -269,14 +253,7 @@ var _ = Describe("Test velaQL rest api", func() { Components: appWithGC.Spec.Components, Policies: appWithGC.Spec.Policies, } - bodyByte, err := json.Marshal(req) - Expect(err).Should(BeNil()) - res, err := http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appWithGC.Name), - "application/json", - bytes.NewBuffer(bodyByte), - ) - Expect(err).ShouldNot(HaveOccurred()) + res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithGC.Name), req) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) @@ -299,7 +276,7 @@ var _ = Describe("Test velaQL rest api", func() { defer queryRes.Body.Close() status := new(Status) - err = json.NewDecoder(queryRes.Body).Decode(status) + err := json.NewDecoder(queryRes.Body).Decode(status) if err != nil { return err } @@ -317,14 +294,7 @@ var _ = Describe("Test velaQL rest api", func() { Components: appWithGC.Spec.Components, Policies: appWithGC.Spec.Policies, } - bodyByte, err = json.Marshal(updateReq) - Expect(err).Should(BeNil()) - res, err = http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appWithGC.Name), - "application/json", - bytes.NewBuffer(bodyByte), - ) - Expect(err).ShouldNot(HaveOccurred()) + res = post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithGC.Name), updateReq) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) @@ -346,7 +316,7 @@ var _ = Describe("Test velaQL rest api", func() { defer queryRes.Body.Close() status := new(Status) - err = json.NewDecoder(queryRes.Body).Decode(status) + err := json.NewDecoder(queryRes.Body).Decode(status) if err != nil { return err } @@ -367,7 +337,7 @@ var _ = Describe("Test velaQL rest api", func() { defer queryRes.Body.Close() status := new(Status) - err = json.NewDecoder(queryRes.Body).Decode(status) + err := json.NewDecoder(queryRes.Body).Decode(status) if err != nil { return err }