Skip to content

Commit

Permalink
Get project information for users from OpenFGA
Browse files Browse the repository at this point in the history
This uses OpenFGA as the authoritative location for user/project
relationships.
  • Loading branch information
JAORMX committed Feb 2, 2024
1 parent 0889f84 commit ae93ea4
Show file tree
Hide file tree
Showing 18 changed files with 148 additions and 282 deletions.
46 changes: 0 additions & 46 deletions cmd/server/app/migrate_up.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (

"github.com/stacklok/minder/internal/authz"
serverconfig "github.com/stacklok/minder/internal/config/server"
"github.com/stacklok/minder/internal/db"
"github.com/stacklok/minder/internal/logger"
)

Expand Down Expand Up @@ -127,55 +126,10 @@ var upCmd = &cobra.Command{
return fmt.Errorf("error preparing authz client: %w", err)
}

store := db.NewStore(dbConn)
if err := migratePermsToFGA(ctx, store, authzw, cmd); err != nil {
return fmt.Errorf("error while migrating permissions to FGA: %w", err)
}

return nil
},
}

func migratePermsToFGA(ctx context.Context, store db.Store, authzw authz.Client, cmd *cobra.Command) error {
cmd.Println("Migrating permissions to FGA...")

var i int32 = 0
for {
userList, err := store.ListUsers(ctx, db.ListUsersParams{Limit: 100, Offset: i})
if err != nil {
return fmt.Errorf("error while listing users: %w", err)
}
i = i + 100
cmd.Printf("Found %d users to migrate\n", len(userList))
if len(userList) == 0 {
break
}

for _, user := range userList {
projs, err := store.GetUserProjects(ctx, user.ID)
if err != nil {
cmd.Printf("Skipping user %d since getting user projects yielded error: %s\n",
user.ID, err)
continue
}

for _, proj := range projs {
cmd.Printf("Migrating user to FGA for project %s\n", proj.ProjectID)
if err := authzw.Write(
ctx, user.IdentitySubject, authz.AuthzRoleAdmin, proj.ProjectID,
); err != nil {
cmd.Printf("Error while writing permission for user %d: %s\n", user.ID, err)
continue
}
}
}
}

cmd.Println("Done migrating permissions to FGA")

return nil
}

func init() {
migrateCmd.AddCommand(upCmd)
}
20 changes: 20 additions & 0 deletions database/migrations/000016_remove_user_project.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- 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.

-- user/projects
CREATE TABLE user_projects (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE
);
15 changes: 15 additions & 0 deletions database/migrations/000016_remove_user_project.up.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.

DROP TABLE IF EXISTS user_projects;
45 changes: 0 additions & 45 deletions database/mock/store.go

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

10 changes: 0 additions & 10 deletions database/query/user_projects.sql

This file was deleted.

8 changes: 0 additions & 8 deletions database/query/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,5 @@ ORDER BY id
LIMIT $2
OFFSET $3;

-- name: ListUsersByProject :many
SELECT users.* FROM users
JOIN user_projects ON users.id = user_projects.user_id
WHERE user_projects.project_id = $1
ORDER BY users.id
LIMIT $2
OFFSET $3;

-- name: CountUsers :one
SELECT COUNT(*) FROM users;
51 changes: 51 additions & 0 deletions internal/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,53 @@ func (a *ClientWrapper) AssignmentsToProject(ctx context.Context, project uuid.U
return assignments, nil
}

// ProjectsForUser lists the projects that the given user has access to
func (a *ClientWrapper) ProjectsForUser(ctx context.Context, sub string) ([]uuid.UUID, error) {
u := getUserForTuple(sub)

var pagesize int32 = 50
var contTok *string = nil

projs := map[string]any{}
projectObj := "project:"

for {
resp, err := a.cli.Read(ctx).Options(fgaclient.ClientReadOptions{
PageSize: &pagesize,
ContinuationToken: contTok,
}).Body(fgaclient.ClientReadRequest{
User: &u,
Object: &projectObj,
}).Execute()
if err != nil {
return nil, fmt.Errorf("unable to read authorization tuples: %w", err)
}

for _, t := range resp.GetTuples() {
k := t.GetKey()

projs[k.GetObject()] = struct{}{}
}

if resp.GetContinuationToken() == "" {
break
}

contTok = &resp.ContinuationToken
}

out := []uuid.UUID{}
for proj := range projs {
u, err := uuid.Parse(getProjectFromTuple(proj))
if err != nil {
continue
}
out = append(out, u)
}

return out, nil
}

func getUserForTuple(user string) string {
return "user:" + user
}
Expand All @@ -388,3 +435,7 @@ func getProjectForTuple(project uuid.UUID) string {
func getUserFromTuple(user string) string {
return strings.TrimPrefix(user, "user:")
}

func getProjectFromTuple(project string) string {
return strings.TrimPrefix(project, "project:")
}
3 changes: 3 additions & 0 deletions internal/authz/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ type Client interface {
// AssignmentsToProject outputs the existing role assignments for a given project.
AssignmentsToProject(ctx context.Context, project uuid.UUID) ([]*minderv1.RoleAssignment, error)

// ProjectsForUser outputs the projects a user has access to.
ProjectsForUser(ctx context.Context, sub string) ([]uuid.UUID, error)

// PrepareForRun allows for any preflight configurations to be done before
// the server is started.
PrepareForRun(ctx context.Context) error
Expand Down
5 changes: 5 additions & 0 deletions internal/authz/mock/noop_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func (_ *NoopClient) AssignmentsToProject(_ context.Context, _ uuid.UUID) ([]*mi
return nil, nil
}

// ProjectsForUser implements authz.Client
func (_ *NoopClient) ProjectsForUser(_ context.Context, _ string) ([]uuid.UUID, error) {
return nil, nil
}

// PrepareForRun implements authz.Client
func (_ *NoopClient) PrepareForRun(_ context.Context) error {
return nil
Expand Down
7 changes: 6 additions & 1 deletion internal/authz/mock/simple_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ func (_ *SimpleClient) DeleteUser(_ context.Context, _ string) error {

// AssignmentsToProject implements authz.Client
func (_ *SimpleClient) AssignmentsToProject(_ context.Context, _ uuid.UUID) ([]*minderv1.RoleAssignment, error) {
return nil, nil
return []*minderv1.RoleAssignment{}, nil
}

// ProjectsForUser implements authz.Client
func (n *SimpleClient) ProjectsForUser(_ context.Context, _ string) ([]uuid.UUID, error) {
return n.Allowed, nil
}

// PrepareForRun implements authz.Client
Expand Down
22 changes: 16 additions & 6 deletions internal/controlplane/handlers_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func EntityContextProjectInterceptor(ctx context.Context, req interface{}, info

server := info.Server.(*Server)

ctx, err := populateEntityContext(ctx, server.store, request)
ctx, err := populateEntityContext(ctx, server.store, server.authzClient, request)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -112,12 +112,17 @@ func ProjectAuthorizationInterceptor(ctx context.Context, req interface{}, info

// populateEntityContext populates the project in the entity context, by looking at the proto context or
// fetching the default project
func populateEntityContext(ctx context.Context, store db.Store, in HasProtoContext) (context.Context, error) {
func populateEntityContext(
ctx context.Context,
store db.Store,
authzClient authz.Client,
in HasProtoContext,
) (context.Context, error) {
if in.GetContext() == nil {
return ctx, fmt.Errorf("context cannot be nil")
}

projectID, err := getProjectFromRequestOrDefault(ctx, store, in)
projectID, err := getProjectFromRequestOrDefault(ctx, store, authzClient, in)
if err != nil {
return ctx, err
}
Expand All @@ -137,7 +142,12 @@ func populateEntityContext(ctx context.Context, store db.Store, in HasProtoConte
return engine.WithEntityContext(ctx, entityCtx), nil
}

func getProjectFromRequestOrDefault(ctx context.Context, store db.Store, in HasProtoContext) (uuid.UUID, error) {
func getProjectFromRequestOrDefault(
ctx context.Context,
store db.Store,
authzClient authz.Client,
in HasProtoContext,
) (uuid.UUID, error) {
// Prefer the context message from the protobuf
if in.GetContext().GetProject() != "" {
requestedProject := in.GetContext().GetProject()
Expand All @@ -154,15 +164,15 @@ func getProjectFromRequestOrDefault(ctx context.Context, store db.Store, in HasP
if err != nil {
return uuid.UUID{}, status.Errorf(codes.NotFound, "user not found")
}
projects, err := store.GetUserProjects(ctx, userInfo.ID)
projects, err := authzClient.ProjectsForUser(ctx, userInfo.IdentitySubject)
if err != nil {
return uuid.UUID{}, status.Errorf(codes.NotFound, "cannot find projects for user")
}

if len(projects) != 1 {
return uuid.UUID{}, status.Errorf(codes.InvalidArgument, "cannot get default project")
}
return projects[0].ID, nil
return projects[0], nil
}

// Permissions API
Expand Down
Loading

0 comments on commit ae93ea4

Please sign in to comment.