Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/rulesets: End to End assignment creation (KHO-172, KHO-173) #162

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ go.work.sum

# env file
.env

*.env_other*
CamPlume1 marked this conversation as resolved.
Show resolved Hide resolved
# Logs
logs
*.log
Expand Down Expand Up @@ -57,4 +57,4 @@ terraform/.terraform/
*.tfstate
*.tfstate.backup
*/terraform.tfstate*
*/terraform.tfvars
*/terraform.tfvars
5 changes: 5 additions & 0 deletions backend/internal/errs/api_err.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ func AssignmentNotAcceptedError() APIError {
return NewAPIError(http.StatusBadRequest, fmt.Errorf("student has not accepted this assignment yet"))
}


func CriticalGithubError() APIError {
return NewAPIError(http.StatusInternalServerError, fmt.Errorf("critical Out of State Error: Github Integration"))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different than GitHubAPI error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to have this one in the intermediary stage before I can properly implement the recovery process.

In my mind this is an error that should not be thrown in any case, and probably represents a panic state


/* Post Requests Only */
func InvalidRequestBody(expected interface{}) APIError {
fieldAcc := make([]string, 0, 10)
Expand Down
13 changes: 12 additions & 1 deletion backend/internal/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package github

import (
"context"

"time"
"github.com/CamPlume1/khoury-classroom/internal/models"
"github.com/google/go-github/github"
)
Expand Down Expand Up @@ -30,6 +30,8 @@ type GitHubAppClient interface { // All methods in the APP client
// Add a repository permission to a user
AssignPermissionToUser(ctx context.Context, ownerName string, repoName string, userName string, permission string) error

CreateDeadlineEnforcement(ctx context.Context, deadline *time.Time, orgName, repoName string) error

// Create instance of template repository
CreateRepoFromTemplate(ctx context.Context, orgName, templateRepoName, newRepoName string) (*models.AssignmentBaseRepo, error)
}
Expand Down Expand Up @@ -116,4 +118,13 @@ type GitHubBaseClient interface { //All methods in the SHARED client

// Remove repository from team
RemoveRepoFromTeam(ctx context.Context, org, teamSlug, owner, repo string) error

//Create push ruleset to protect .github folders
CreatePushRuleset(ctx context.Context, orgName, repoName string) error

//Create rulesets to protect corresponding branches
CreateBranchRuleset(ctx context.Context, orgName, repoName string) error

//Creates PR enforcements
CreatePREnforcement(ctx context.Context, orgName, repoName string) error
}
145 changes: 144 additions & 1 deletion backend/internal/github/sharedclient/sharedclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package sharedclient

import (
"context"
"encoding/base64"
"fmt"

"time"
"github.com/CamPlume1/khoury-classroom/internal/errs"
"github.com/CamPlume1/khoury-classroom/internal/models"
"github.com/CamPlume1/khoury-classroom/internal/utils"
"github.com/google/go-github/github"
)

Expand Down Expand Up @@ -183,6 +185,147 @@ func (api *CommonAPI) GetUser(ctx context.Context, userName string) (*github.Use
return user, err
}



func (api *CommonAPI) createRuleSet(ctx context.Context, ruleset interface{}, orgName, repoName string) error {
endpoint := fmt.Sprintf("/repos/%s/%s/rulesets", orgName, repoName)
req, err := api.Client.NewRequest("POST", endpoint, ruleset)
if err != nil {
return err
}
_, err = api.Client.Do(ctx, req, nil)
return err
}

//Given a repo name and org name, create a push ruleset to protect the .github directory
func (api *CommonAPI) CreatePushRuleset(ctx context.Context, orgName, repoName string) error {
body := map[string]interface{}{
"name": "Restrict .github Directory Edits: Preserves Submission Deadline",
"target": "push",
"enforcement": "active",
"rules": []interface{}{
map[string]interface{}{
"type": "file_path_restriction",
"parameters": map[string]interface{}{
"restricted_file_paths": []string{".github/**/*"},
},
},
},
}
return api.createRuleSet(ctx, body, orgName, repoName)
}
CamPlume1 marked this conversation as resolved.
Show resolved Hide resolved



func (api *CommonAPI) CreateBranchRuleset(ctx context.Context, orgName, repoName string) error {
body := map[string]interface{}{
"name": "Feedback and Main Branch Protedtion: PR Enforcement",
"target": "branch",
"enforcement": "active",
"conditions": map[string]interface{}{
"ref_name": map[string]interface{}{
"exclude": []interface{}{},
"include": []interface{}{"refs/heads/feedback", "~DEFAULT_BRANCH"},
},
},
"rules": []interface{}{
map[string]interface{}{
"type": "non_fast_forward",
},
map[string]interface{}{
"type": "deletion",
},
map[string]interface{}{
"type": "update",
"parameters": map[string]interface{}{
"update_allows_fetch_and_merge": true,
},
},

map[string]interface{}{
"type": "pull_request",
"parameters" : map[string]interface{}{
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"automatic_copilot_code_review_enabled": false,
},
},
map[string]interface{}{
"type": "required_status_checks",
"parameters": map[string]interface{}{
"strict_required_status_checks_policy": false,
"do_not_enforce_on_create": false,
"required_status_checks": []map[string]string{
map[string]string{
"context": "check-date",
},
map[string]string{
"context": "check-target",
},

},

},
},
},
}
return api.createRuleSet(ctx, body, orgName, repoName)
CamPlume1 marked this conversation as resolved.
Show resolved Hide resolved
}





func (api *CommonAPI) CreateDeadlineEnforcement(ctx context.Context, deadline *time.Time, orgName, repoName string) error {
addition := models.RepositoryAddition{
FilePath: ".github/workflows/deadline.yml",
RepoName: repoName,
OwnerName: orgName,
DestinationBranch: "main",
CamPlume1 marked this conversation as resolved.
Show resolved Hide resolved
Content: utils.ActionWithDeadline(deadline),
CommitMessage: "Deadline enforcement GH action files",
}
return api.EditRepository(ctx, &addition)

}


func (api *CommonAPI) CreatePREnforcement(ctx context.Context, orgName, repoName string) error {

addition := models.RepositoryAddition{
FilePath: ".github/workflows/branchProtections.yml",
RepoName: repoName,
OwnerName: orgName,
DestinationBranch: "main",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take in DestinationBranch as a parameter. It will not always be main (master, for example). Can be dynamically determined with pushEvent.Repo.MasterBranch in webhooks file.

Content: utils.TargetBranchProtectionAction(),
CommitMessage: "Deadline enforcement GH action files",
}
return api.EditRepository(ctx, &addition)

}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above for CreateDeadlineEnforcement and CreatePREnforcement with EditRepository.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved w struct usage


func (api *CommonAPI) EditRepository(ctx context.Context, addition *models.RepositoryAddition) error {
endpoint := fmt.Sprintf("/repos/%s/%s/contents/%s", addition.OwnerName, addition.RepoName, addition.FilePath)
encodedContent := base64.StdEncoding.EncodeToString([]byte(addition.Content))

body := map[string]interface{}{
"message": addition.CommitMessage,
"content": encodedContent,
"branch": addition.DestinationBranch,
}
req, err := api.Client.NewRequest("PUT", endpoint, body)
if err != nil {
return err
}
_, err = api.Client.Do(ctx, req, nil)
return err
}



func (api *CommonAPI) InviteUserToOrganization(ctx context.Context, orgName string, userID int64) error {
body := map[string]interface{}{
"invitee_id": userID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func (s *AssignmentService) createAssignment() fiber.Handler {
return errs.InvalidRequestBody(assignmentData)
}


// Error if assignment already exists
existingAssignment, err := s.store.GetAssignmentByNameAndClassroomID(c.Context(), assignmentData.Name, assignmentData.ClassroomID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
Expand Down Expand Up @@ -148,6 +149,7 @@ func (s *AssignmentService) generateAssignmentToken() fiber.Handler {

// Uses an assignment token to accept an assignment.
func (s *AssignmentService) useAssignmentToken() fiber.Handler {
//@CamTODO-> Downgrade student access
return func(c *fiber.Ctx) error {
token := c.Params("token")
if token == "" {
Expand Down Expand Up @@ -225,6 +227,12 @@ func (s *AssignmentService) useAssignmentToken() fiber.Handler {
initialDelay *= 2
}

//@CamTODO: Get rid of happy path here with repo get fail
CamPlume1 marked this conversation as resolved.
Show resolved Hide resolved
err = client.CreateBranchRuleset(c.Context(), classroom.OrgName, forkName)
if err != nil {
return errs.CriticalGithubError()
}

// Remove student team's access to forked repo
err = client.RemoveRepoFromTeam(c.Context(), classroom.OrgName, *classroom.StudentTeamName, classroom.OrgName, *studentWorkRepo.Name)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/handlers/classrooms/assignments/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

type AssignmentService struct {
store storage.Storage
userCfg *config.GitHubUserClient
store storage.Storage
userCfg *config.GitHubUserClient
appClient github.GitHubAppClient
}

Expand Down
11 changes: 8 additions & 3 deletions backend/internal/handlers/hello/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import (
"github.com/gofiber/fiber/v2"
)

// Create HelloGroup fiber route group
// Routes initializes the "hello" and "hello_protected" route groups in the Fiber application.
// It sets up middleware for protected routes and registers endpoints.
//
// Parameters:
// - app: The Fiber application instance to which the routes will be added.
// - params: A Params struct containing configuration such as the data store and JWT secret.
func Routes(app *fiber.App, params types.Params) {
service := newService(params.Store)

Expand All @@ -18,10 +23,10 @@ func Routes(app *fiber.App, params types.Params) {
// Register Middleware
protected.Use(middleware.Protected(params.UserCfg.JWTSecret))

//Unprotected Routes
// Unprotected Routes
unprotected := app.Group("/hello")

//Endpoints
// Endpoints
protected.Get("/world", service.HelloWorld)
unprotected.Get("/world", service.HelloWorld)
}
4 changes: 3 additions & 1 deletion backend/internal/handlers/organizations/organization.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package organizations

import (
//"fmt"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"strconv"
"strings"

Expand All @@ -17,18 +18,19 @@ func (service *OrganizationService) GetOrgsAndClassrooms() fiber.Handler {
}
}

//Get all organizations a user is in
func (service *OrganizationService) GetUserOrgs() fiber.Handler {
return func(c *fiber.Ctx) error {
client, err := middleware.GetClient(c, service.store, service.userCfg)
if err != nil {
return errs.GithubClientError(err)
}

// Retrieve orgs from user client
orgs, err := client.GetUserOrgs(c.Context())
if err != nil {
return errs.GithubAPIError(err)
}

return c.Status(fiber.StatusOK).JSON(fiber.Map{"orgs": orgs})
}
}
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/handlers/organizations/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"github.com/CamPlume1/khoury-classroom/internal/storage"
)


//Service Declaration
type OrganizationService struct {
store storage.Storage
appClient github.GitHubAppClient
userCfg *config.GitHubUserClient
}

//Service constructor
func NewOrganizationService(
store storage.Storage,
appClient github.GitHubAppClient,
Expand Down
Loading
Loading