Anukul Sangwan 4 лет назад
Родитель
Сommit
5cd915f9f4

+ 79 - 11
api/server/authz/git_installation.go

@@ -5,12 +5,15 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/google/go-github/github"
+	"golang.org/x/oauth2"
+
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
-	"gorm.io/gorm"
+	"github.com/porter-dev/porter/internal/oauth"
 )
 
 type GitInstallationScopedFactory struct {
@@ -33,24 +36,22 @@ type GitInstallationScopedMiddleware struct {
 }
 
 func (p *GitInstallationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// read the project to check scopes
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	// read the user to perform authorization
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
 	// get the registry id from the URL param context
 	reqScopes, _ := r.Context().Value(types.RequestScopeCtxKey).(map[types.PermissionScope]*types.RequestAction)
 	gitInstallationID := reqScopes[types.GitInstallationScope].Resource.UInt
 
-	gitInstallation, err := p.config.Repo.GithubAppInstallation().ReadGithubAppInstallation(proj.ID, gitInstallationID)
+	gitInstallation, err := p.config.Repo.GithubAppInstallation().ReadGithubAppInstallationByInstallationID(gitInstallationID)
 
 	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(
-				fmt.Errorf("github app installation with id %d not found in project %d", gitInstallationID, proj.ID),
-			))
-		} else {
-			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err))
-		}
+		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err))
+		return
+	}
 
+	if err := p.doesUserHaveGitInstallationAccess(user.GithubAppIntegrationID, gitInstallationID); err != nil {
+		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -62,3 +63,70 @@ func (p *GitInstallationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *ht
 func NewGitInstallationContext(ctx context.Context, ga *integrations.GithubAppInstallation) context.Context {
 	return context.WithValue(ctx, types.GitInstallationScope, ga)
 }
+
+// DoesUserHaveGitInstallationAccess checks that a user has access to an installation id
+// by ensuring the installation id exists for one org or account they have access to
+// note that this makes a github API request, but the endpoint is fast so this doesn't add
+// much overhead
+func (p *GitInstallationScopedMiddleware) doesUserHaveGitInstallationAccess(githubIntegrationID, gitInstallationID uint) error {
+	oauthInt, err := p.config.Repo.GithubAppOAuthIntegration().ReadGithubAppOauthIntegration(githubIntegrationID)
+
+	if err != nil {
+		return err
+	}
+
+	if _, _, err = oauth.GetAccessToken(oauthInt.SharedOAuthModel,
+		p.config.GithubConf,
+		oauth.MakeUpdateGithubAppOauthIntegrationFunction(oauthInt, p.config.Repo)); err != nil {
+		return err
+	}
+
+	client := github.NewClient(p.config.GithubConf.Client(oauth2.NoContext, &oauth2.Token{
+		AccessToken:  string(oauthInt.AccessToken),
+		RefreshToken: string(oauthInt.RefreshToken),
+		TokenType:    "Bearer",
+	}))
+
+	accountIDs := make([]int64, 0)
+
+	AuthUser, _, err := client.Users.Get(context.Background(), "")
+
+	if err != nil {
+		return err
+	}
+
+	accountIDs = append(accountIDs, *AuthUser.ID)
+
+	opts := &github.ListOptions{
+		PerPage: 100,
+		Page:    1,
+	}
+
+	for {
+		orgs, pages, err := client.Organizations.List(context.Background(), "", opts)
+
+		if err != nil {
+			return err
+		}
+
+		for _, org := range orgs {
+			accountIDs = append(accountIDs, *org.ID)
+		}
+
+		if pages.NextPage == 0 {
+			break
+		}
+	}
+
+	installations, err := p.config.Repo.GithubAppInstallation().ReadGithubAppInstallationByAccountIDs(accountIDs)
+
+	for _, installation := range installations {
+		if uint(installation.InstallationID) == gitInstallationID {
+			return nil
+		}
+	}
+
+	return apierrors.NewErrForbidden(
+		fmt.Errorf("user does not have access to github app installation %d", gitInstallationID),
+	)
+}

+ 1 - 1
api/server/handlers/gitinstallation/helpers.go

@@ -62,7 +62,7 @@ func GetGithubAppClientFromRequest(config *config.Config, r *http.Request) (*git
 	itr, err := ghinstallation.NewKeyFromFile(
 		http.DefaultTransport,
 		config.GithubAppConf.AppID,
-		int64(ga.ID),
+		ga.InstallationID,
 		config.GithubAppConf.SecretPath,
 	)
 

+ 2 - 1
api/server/handlers/release/create.go

@@ -223,6 +223,7 @@ func createGitAction(
 		PorterToken:            encoded,
 		Version:                "v0.1.0",
 		ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
+		DryRun:                 release == nil,
 	}
 
 	workflowYAML, err := gaRunner.Setup()
@@ -231,7 +232,7 @@ func createGitAction(
 		return nil, nil, err
 	}
 
-	if !request.ShouldCreateWorkflow {
+	if gaRunner.DryRun {
 		return nil, workflowYAML, nil
 	}
 

+ 58 - 0
api/server/handlers/release/get_gha_template.go

@@ -0,0 +1,58 @@
+package release
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type GetGHATemplateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetGHATemplateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetGHATemplateHandler {
+	return &GetGHATemplateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetGHATemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+
+	request := &types.GetGHATemplateRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	_, workflowYAML, err := createGitAction(
+		c.Config(),
+		user.ID,
+		cluster.ProjectID,
+		cluster.ID,
+		request.GithubActionConfig,
+		request.ReleaseName,
+		namespace,
+		nil,
+	)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := types.GetGHATemplateResponse(workflowYAML)
+
+	c.WriteResult(w, r, res)
+}

+ 30 - 0
api/server/router/release.go

@@ -380,6 +380,36 @@ func getReleaseRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/gha_template -> release.NewGetGHATemplateHandler
+	getGHATemplateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/releases/gha_template",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	getGHATemplateHandler := release.NewGetGHATemplateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getGHATemplateEndpoint,
+		Handler:  getGHATemplateHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version}/rollback ->
 	// release.NewRollbackReleaseHandler
 	rollbackEndpoint := factory.NewAPIEndpoint(

+ 1 - 1
api/server/router/user.go

@@ -350,7 +350,7 @@ func getUserRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: "/integrations/github-app/callback",
+				RelativePath: "/integrations/github-app/accounts",
 			},
 			Scopes: []types.PermissionScope{types.UserScope},
 		},

+ 7 - 0
api/types/release.go

@@ -72,3 +72,10 @@ const URLParamToken URLParam = "token"
 type WebhookRequest struct {
 	Commit string `schema:"commit"`
 }
+
+type GetGHATemplateRequest struct {
+	ReleaseName        string                        `json:"release_name"`
+	GithubActionConfig *CreateGitActionConfigRequest `json:"github_action_config" form:"required"`
+}
+
+type GetGHATemplateResponse string

+ 10 - 11
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -67,7 +67,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
     api
       .getBranchContents(
         "<token>",
-        { dir: this.state.currentDir },
+        { dir: this.state.currentDir || "./" },
         {
           project_id: currentProject.id,
           git_repo_id: actionConfig.git_repo_id,
@@ -81,14 +81,14 @@ export default class ContentsList extends Component<PropsType, StateType> {
         let files = [] as FileType[];
         let folders = [] as FileType[];
         res.data.map((x: FileType, i: number) => {
-          x.Type === "dir" ? folders.push(x) : files.push(x);
+          x.type === "dir" ? folders.push(x) : files.push(x);
         });
 
         folders.sort((a: FileType, b: FileType) => {
-          return a.Path < b.Path ? 1 : 0;
+          return a.path < b.path ? 1 : 0;
         });
         files.sort((a: FileType, b: FileType) => {
-          return a.Path < b.Path ? 1 : 0;
+          return a.path < b.path ? 1 : 0;
         });
         let contents = folders.concat(files);
 
@@ -166,17 +166,16 @@ export default class ContentsList extends Component<PropsType, StateType> {
     } else if (error || !contents) {
       return <LoadingWrapper>Error loading repo contents.</LoadingWrapper>;
     }
-
     return contents.map((item: FileType, i: number) => {
-      let splits = item.Path.split("/");
+      let splits = item.path.split("/");
       let fileName = splits[splits.length - 1];
-      if (item.Type === "dir") {
+      if (item.type === "dir") {
         return (
           <Item
             key={i}
-            isSelected={item.Path === this.state.currentDir}
+            isSelected={item.path === this.state.currentDir}
             lastItem={i === contents.length - 1}
-            onClick={() => this.setSubdirectory(item.Path)}
+            onClick={() => this.setSubdirectory(item.path)}
           >
             <img src={folder} />
             {fileName}
@@ -190,7 +189,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
             key={i}
             lastItem={i === contents.length - 1}
             isADocker
-            onClick={() => this.props.setDockerfilePath(item.Path)}
+            onClick={() => this.props.setDockerfilePath(item.path)}
           >
             <img src={file} />
             {fileName}
@@ -236,7 +235,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
   handleContinue = () => {
     let dockerfiles = [] as string[];
     this.state.contents.forEach((item: FileType, i: number) => {
-      let splits = item.Path.split("/");
+      let splits = item.path.split("/");
       let fileName = splits[splits.length - 1];
       if (fileName.includes("Dockerfile")) {
         dockerfiles.push(fileName);

+ 1 - 1
dashboard/src/components/repo-selector/RepoList.tsx

@@ -42,7 +42,7 @@ const RepoList: React.FC<Props> = ({
   // TODO: Try to unhook before unmount
   useEffect(() => {
     api
-      .getGithubAccess("<token>", {}, {})
+      .getGithubAccounts("<token>", {}, {})
       .then(({ data }) => {
         setAccessData(data);
         setAccessLoading(false);

+ 2 - 2
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -213,8 +213,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
           api
             .createSubdomain(
               "<token>",
-              {
-              },
+              {},
               {
                 id: currentProject.id,
                 cluster_id: currentCluster.id,
@@ -326,6 +325,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
       return (
         <WorkflowPage
           name={templateName}
+          namespace={selectedNamespace}
           fullActionConfig={fullActionConfig}
           shouldCreateWorkflow={shouldCreateWorkflow}
           setShouldCreateWorkflow={setShouldCreateWorkflow}

+ 13 - 5
dashboard/src/main/home/launch/launch-flow/WorkflowPage.tsx

@@ -12,6 +12,7 @@ import SaveButton from "../../../../components/SaveButton";
 
 type PropsType = {
   name: string;
+  namespace: string;
   fullActionConfig: FullActionConfigType;
   shouldCreateWorkflow: boolean;
   setShouldCreateWorkflow: (x: (prevState: boolean) => boolean) => void;
@@ -29,11 +30,18 @@ const WorkflowPage: React.FC<PropsType> = (props) => {
     const { currentCluster, currentProject } = context;
 
     api
-      .generateGHAWorkflow("<token>", props.fullActionConfig, {
-        name: props.name,
-        cluster_id: currentCluster.id,
-        project_id: currentProject.id,
-      })
+      .getGHAWorkflowTemplate(
+        "<token>",
+        {
+          name: props.name,
+          github_action_config: props.fullActionConfig,
+        },
+        {
+          namespace: props.namespace,
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+        }
+      )
       .then((res) => {
         setWorkflowYAML(res.data);
         setIsLoading(false);

+ 1 - 1
dashboard/src/main/home/modals/AccountSettingsModal.tsx

@@ -31,7 +31,7 @@ const AccountSettingsModal = () => {
 
   useEffect(() => {
     api
-      .getGithubAccess("<token>", {}, {})
+      .getGithubAccounts("<token>", {}, {})
       .then(({ data }) => {
         setAccessData(data);
         setAccessLoading(false);

+ 20 - 17
dashboard/src/shared/api.tsx

@@ -274,17 +274,20 @@ const getNotificationConfig = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/releases/${name}/notifications`;
 });
 
-const generateGHAWorkflow = baseApi<
-  FullActionConfigType,
+const getGHAWorkflowTemplate = baseApi<
+  {
+    name: string;
+    github_action_config: FullActionConfigType;
+  },
   {
     cluster_id: number;
     project_id: number;
-    name: string;
+    namespace: string;
   }
 >("POST", (pathParams) => {
-  const { name, cluster_id, project_id } = pathParams;
+  const { cluster_id, project_id, namespace } = pathParams;
 
-  return `/api/projects/${project_id}/ci/actions/generate?cluster_id=${cluster_id}&name=${name}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/releases/gha_template`;
 });
 
 const deployTemplate = baseApi<
@@ -676,15 +679,15 @@ const getReleaseToken = baseApi<
 });
 
 const destroyInfra = baseApi<
-{
-  name: string;
-},
-{
-  project_id: number;
-  infra_id: number;
-}
+  {
+    name: string;
+  },
+  {
+    project_id: number;
+    infra_id: number;
+  }
 >("DELETE", (pathParams) => {
-return `/api/projects/${pathParams.project_id}/infras/${pathParams.infra_id}`;
+  return `/api/projects/${pathParams.project_id}/infras/${pathParams.infra_id}`;
 });
 
 const getRepoIntegrations = baseApi("GET", "/api/integrations/repo");
@@ -748,8 +751,8 @@ const linkGithubProject = baseApi<
   return `/api/oauth/projects/${pathParams.project_id}/github`;
 });
 
-const getGithubAccess = baseApi<{}, {}>("GET", () => {
-  return `/api/integrations/github-app/access`;
+const getGithubAccounts = baseApi<{}, {}>("GET", () => {
+  return `/api/integrations/github-app/accounts`;
 });
 
 const logInUser = baseApi<{
@@ -1059,7 +1062,7 @@ export default {
   getClusterNodes,
   getClusterNode,
   getConfigMap,
-  generateGHAWorkflow,
+  getGHAWorkflowTemplate,
   getGitRepoList,
   getGitRepos,
   getImageRepos,
@@ -1092,7 +1095,7 @@ export default {
   getTemplateUpgradeNotes,
   getTemplates,
   linkGithubProject,
-  getGithubAccess,
+  getGithubAccounts,
   listConfigMaps,
   logInUser,
   logOutUser,

+ 2 - 2
dashboard/src/shared/types.tsx

@@ -210,8 +210,8 @@ export interface RepoType {
 }
 
 export interface FileType {
-  Path: string;
-  Type: string;
+  path: string;
+  type: string;
 }
 
 export interface ProjectType {

+ 1 - 1
docs/developing/backend-refactor-status.md

@@ -35,7 +35,7 @@
 | <li>- [X] `DELETE /api/projects/{project_id}`                                                                               | AB          |                 |             |                  |
 | <li>- [x] `GET /api/projects/{project_id}`                                                                                  | AB          |                 |             |                  |
 | <li>- [X] `POST /api/projects/{project_id}/ci/actions/create`                                                               | N/A         |                 |             |                  |
-| <li>- [ ] `POST /api/projects/{project_id}/ci/actions/generate`                                                             |             |                 |             |                  |
+| <li>- [x] `POST /api/projects/{project_id}/ci/actions/generate`                                                             | AS          | yes             |             | yes              |
 | <li>- [x] `GET /api/projects/{project_id}/clusters`                                                                         | AB          |                 |             | yes              |
 | <li>- [X] `POST /api/projects/{project_id}/clusters`                                                                        | AB          |                 |             |                  |
 | <li>- [X] `POST /api/projects/{project_id}/clusters/candidates`                                                             | AB          |                 |             |                  |

+ 3 - 3
internal/integrations/ci/actions/actions.go

@@ -46,7 +46,7 @@ type GithubActions struct {
 	defaultBranch string
 	Version       string
 
-	ShouldGenerateOnly   bool
+	DryRun               bool
 	ShouldCreateWorkflow bool
 }
 
@@ -70,7 +70,7 @@ func (g *GithubActions) Setup() ([]byte, error) {
 
 	g.defaultBranch = repo.GetDefaultBranch()
 
-	if !g.ShouldGenerateOnly {
+	if !g.DryRun {
 		// create porter token secret
 		if err := g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken); err != nil {
 			return nil, err
@@ -83,7 +83,7 @@ func (g *GithubActions) Setup() ([]byte, error) {
 		return nil, err
 	}
 
-	if !g.ShouldGenerateOnly && g.ShouldCreateWorkflow {
+	if !g.DryRun && g.ShouldCreateWorkflow {
 		_, err = g.commitGithubFile(client, g.getPorterYMLFileName(), workflowYAML)
 		if err != nil {
 			return workflowYAML, err

+ 3 - 3
internal/repository/gorm/auth.go

@@ -1106,11 +1106,11 @@ func (repo *GithubAppInstallationRepository) CreateGithubAppInstallation(am *int
 	return am, nil
 }
 
-// ReadGithubAppInstallation finds a GithubAppInstallation by id
-func (repo *GithubAppInstallationRepository) ReadGithubAppInstallation(projectID, gaID uint) (*ints.GithubAppInstallation, error) {
+// ReadGithubAppInstallationByInstallationID finds a GithubAppInstallation by id
+func (repo *GithubAppInstallationRepository) ReadGithubAppInstallationByInstallationID(gaID uint) (*ints.GithubAppInstallation, error) {
 	ret := &ints.GithubAppInstallation{}
 
-	if err := repo.db.Where("project_id = ? AND id = ?", projectID, gaID).First(&ret).Error; err != nil {
+	if err := repo.db.Where("installation_id = ?", gaID).First(&ret).Error; err != nil {
 		return nil, err
 	}
 

+ 1 - 1
internal/repository/integrations.go

@@ -72,7 +72,7 @@ type GCPIntegrationRepository interface {
 // GithubAppInstallationRepository represents the set of queries for github app installations
 type GithubAppInstallationRepository interface {
 	CreateGithubAppInstallation(am *ints.GithubAppInstallation) (*ints.GithubAppInstallation, error)
-	ReadGithubAppInstallation(projectID, gaID uint) (*ints.GithubAppInstallation, error)
+	ReadGithubAppInstallationByInstallationID(gaID uint) (*ints.GithubAppInstallation, error)
 	ReadGithubAppInstallationByAccountID(accountID int64) (*ints.GithubAppInstallation, error)
 	ReadGithubAppInstallationByAccountIDs(accountIDs []int64) ([]*ints.GithubAppInstallation, error)
 	DeleteGithubAppInstallationByAccountID(accountID int64) error