Skip to content

Commit

Permalink
Add subproject creation/deletion operations (#2556)
Browse files Browse the repository at this point in the history
* Add subproject creation operations

Signed-off-by: Juan Antonio Osorio <[email protected]>

Add sub-project deletion capabilities

Signed-off-by: Juan Antonio Osorio <[email protected]>

Handle name constraints more gracefully

Signed-off-by: Juan Antonio Osorio <[email protected]>

Add sub-projects to project list output

Signed-off-by: Juan Antonio Osorio <[email protected]>

Aestetic changes to the CLI

Signed-off-by: Juan Antonio Osorio <[email protected]>

User visible errors are important!

Signed-off-by: Juan Antonio Osorio <[email protected]>

Use viper for reading projects in project sub-commands

Signed-off-by: Juan Antonio Osorio <[email protected]>

rename

Signed-off-by: Juan Antonio Osorio <[email protected]>

Use recursive algorithm to traverse project trees instead of serial

Signed-off-by: Juan Antonio Osorio <[email protected]>

* Add feature flags for project hierarchy operations

Signed-off-by: Juan Antonio Osorio <[email protected]>

---------

Signed-off-by: Juan Antonio Osorio <[email protected]>
  • Loading branch information
JAORMX authored Mar 12, 2024
1 parent 9886d13 commit c0604ea
Show file tree
Hide file tree
Showing 21 changed files with 2,448 additions and 956 deletions.
100 changes: 100 additions & 0 deletions cmd/cli/app/project/project_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Copyright 2024 Stacklok, Inc.
//
// 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 project

import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/grpc"

"github.com/stacklok/minder/cmd/cli/app"
"github.com/stacklok/minder/internal/util"
"github.com/stacklok/minder/internal/util/cli"
"github.com/stacklok/minder/internal/util/cli/table"
"github.com/stacklok/minder/internal/util/cli/table/layouts"
minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
)

// projectCreateCmd is the command for creating sub-projects
var projectCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a sub-project within a minder control plane",
Long: `The list command lists the projects available to you within a minder control plane.`,
RunE: cli.GRPCClientWrapRunE(createCommand),
}

// listCommand is the command for listing projects
func createCommand(ctx context.Context, cmd *cobra.Command, conn *grpc.ClientConn) error {
client := minderv1.NewProjectsServiceClient(conn)

format := viper.GetString("output")
project := viper.GetString("project")
name := viper.GetString("name")

// No longer print usage on returned error, since we've parsed our inputs
// See https://github.com/spf13/cobra/issues/340#issuecomment-374617413
cmd.SilenceUsage = true

resp, err := client.CreateProject(ctx, &minderv1.CreateProjectRequest{
Context: &minderv1.Context{
Project: &project,
},
Name: name,
})
if err != nil {
return cli.MessageAndError("Error creating sub-project", err)
}

switch format {
case app.JSON:
out, err := util.GetJsonFromProto(resp)
if err != nil {
return cli.MessageAndError("Error getting json from proto", err)
}
cmd.Println(out)
case app.YAML:
out, err := util.GetYamlFromProto(resp)
if err != nil {
return cli.MessageAndError("Error getting yaml from proto", err)
}
cmd.Println(out)
case app.Table:
t := table.New(table.Simple, layouts.Default, []string{"ID", "Name"})
t.AddRow(resp.Project.ProjectId, resp.Project.Name)
t.Render()
default:
return fmt.Errorf("unsupported output format: %s", format)
}

return nil
}

func init() {
ProjectCmd.AddCommand(projectCreateCmd)

projectCreateCmd.Flags().StringP("project", "j", "", "The project to create the sub-project within")
projectCreateCmd.Flags().StringP("name", "n", "", "The name of the project to create")
// mark as required
if err := projectCreateCmd.MarkFlagRequired("name"); err != nil {
panic(err)
}
projectCreateCmd.Flags().StringP("output", "o", app.Table,
fmt.Sprintf("Output format (one of %s)", strings.Join(app.SupportedOutputFormats(), ",")))
}
69 changes: 69 additions & 0 deletions cmd/cli/app/project/project_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// Copyright 2024 Stacklok, Inc.
//
// 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 project

import (
"context"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/grpc"

"github.com/stacklok/minder/internal/util/cli"
minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
)

// projectDeleteCmd is the command for deleting sub-projects
var projectDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a sub-project within a minder control plane",
Long: `Delete a sub-project within a minder control plane`,
RunE: cli.GRPCClientWrapRunE(deleteCommand),
}

// listCommand is the command for listing projects
func deleteCommand(ctx context.Context, cmd *cobra.Command, conn *grpc.ClientConn) error {
client := minderv1.NewProjectsServiceClient(conn)

project := viper.GetString("project")

// No longer print usage on returned error, since we've parsed our inputs
// See https://github.com/spf13/cobra/issues/340#issuecomment-374617413
cmd.SilenceUsage = true

resp, err := client.DeleteProject(ctx, &minderv1.DeleteProjectRequest{
Context: &minderv1.Context{
Project: &project,
},
})
if err != nil {
return cli.MessageAndError("Error deleting sub-project", err)
}

cmd.Println("Successfully deleted profile with id:", resp.ProjectId)

return nil
}

func init() {
ProjectCmd.AddCommand(projectDeleteCmd)

projectDeleteCmd.Flags().StringP("project", "j", "", "The sub-project to delete")
// mark as required
if err := projectDeleteCmd.MarkFlagRequired("project"); err != nil {
panic(err)
}
}
15 changes: 15 additions & 0 deletions database/migrations/000027_project_hierarchy_feature.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Copyright 2024 Stacklok, Inc
--
-- 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.

DELETE FROM features WHERE name = 'project_hierarchy_operations_enabled';
18 changes: 18 additions & 0 deletions database/migrations/000027_project_hierarchy_feature.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Copyright 2024 Stacklok, Inc
--
-- 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.

-- hierarchy operations feature
INSERT INTO features(name, settings)
VALUES ('project_hierarchy_operations_enabled', '{}')
ON CONFLICT DO NOTHING;
47 changes: 47 additions & 0 deletions docs/docs/ref/proto.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 66 additions & 7 deletions internal/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,25 @@ func (a *ClientWrapper) Check(ctx context.Context, action string, project uuid.U

// Write persists the given role for the given user and project
func (a *ClientWrapper) Write(ctx context.Context, user string, role Role, project uuid.UUID) error {
resp, err := a.cli.WriteTuples(ctx).Options(fgaclient.ClientWriteOptions{}).Body([]fgasdk.TupleKey{
{
User: getUserForTuple(user),
Relation: role.String(),
Object: getProjectForTuple(project),
},
}).Execute()
return a.write(ctx, fgasdk.TupleKey{
User: getUserForTuple(user),
Relation: role.String(),
Object: getProjectForTuple(project),
})
}

// Adopt writes a relationship between the parent and child projects
func (a *ClientWrapper) Adopt(ctx context.Context, parent, child uuid.UUID) error {
return a.write(ctx, fgasdk.TupleKey{
User: getProjectForTuple(parent),
Relation: "parent",
Object: getProjectForTuple(child),
})
}

func (a *ClientWrapper) write(ctx context.Context, t fgasdk.TupleKey) error {
resp, err := a.cli.WriteTuples(ctx).Options(fgaclient.ClientWriteOptions{}).
Body([]fgasdk.TupleKey{t}).Execute()
if err != nil && strings.Contains(err.Error(), "already exists") {
return nil
} else if err != nil {
Expand All @@ -285,6 +297,11 @@ func (a *ClientWrapper) Delete(ctx context.Context, user string, role Role, proj
return a.doDelete(ctx, getUserForTuple(user), role.String(), getProjectForTuple(project))
}

// Orphan removes the relationship between the parent and child projects
func (a *ClientWrapper) Orphan(ctx context.Context, parent, child uuid.UUID) error {
return a.doDelete(ctx, getProjectForTuple(parent), "parent", getProjectForTuple(child))
}

// doDelete wraps the OpenFGA DeleteTuples call and handles edge cases as needed. It takes
// the user, role, and project as tuple-formatted strings.
func (a *ClientWrapper) doDelete(ctx context.Context, user string, role string, project string) error {
Expand Down Expand Up @@ -418,12 +435,54 @@ func (a *ClientWrapper) ProjectsForUser(ctx context.Context, sub string) ([]uuid
if err != nil {
continue
}

out = append(out, u)

children, err := a.traverseProjectsForParent(ctx, u)
if err != nil {
return nil, err
}

out = append(out, children...)
}

return out, nil
}

// traverseProjectsForParent is a recursive function that traverses the project
// hierarchy to find all projects that the parent project has access to.
func (a *ClientWrapper) traverseProjectsForParent(ctx context.Context, parent uuid.UUID) ([]uuid.UUID, error) {
projects := []uuid.UUID{}

resp, err := a.cli.ListObjects(ctx).Body(fgaclient.ClientListObjectsRequest{
User: getProjectForTuple(parent),
Relation: "parent",
Type: "project",
}).Execute()

if err != nil {
return nil, fmt.Errorf("unable to read authorization tuples: %w", err)
}

for _, obj := range resp.GetObjects() {
u, err := uuid.Parse(getProjectFromTuple(obj))
if err != nil {
continue
}
projects = append(projects, u)
}

for _, proj := range projects {
children, err := a.traverseProjectsForParent(ctx, proj)
if err != nil {
return nil, err
}
projects = append(projects, children...)
}

return projects, nil
}

func getUserForTuple(user string) string {
return "user:" + user
}
Expand Down
6 changes: 6 additions & 0 deletions internal/authz/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,10 @@ type Client interface {

// MigrateUp runs the authz migrations
MigrateUp(ctx context.Context) error

// Adopt stores an authorization relationship from one project to another
Adopt(ctx context.Context, parent, child uuid.UUID) error

// Orphan removes an authorization relationship from one project to another
Orphan(ctx context.Context, parent, child uuid.UUID) error
}
Loading

0 comments on commit c0604ea

Please sign in to comment.