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 1, 2024
1 parent 0889f84 commit 1d8255d
Show file tree
Hide file tree
Showing 18 changed files with 120 additions and 273 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)
}
Empty file.
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 2023 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;
64 changes: 64 additions & 0 deletions internal/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,47 @@ 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{}{}
}
}

out := []uuid.UUID{}

Check failure on line 409 in internal/authz/authz.go

View workflow job for this annotation

GitHub Actions / lint / Go Lint

unreachable: unreachable code (govet)
for proj := range projs {
uuid, err := uuid.Parse(getProjectFromTuple(proj))

Check warning on line 411 in internal/authz/authz.go

View workflow job for this annotation

GitHub Actions / lint / Go Lint

import-shadowing: The name 'uuid' shadows an import name (revive)
if err != nil {
continue
}
out = append(out, uuid)
}

return out, nil
}

func getUserForTuple(user string) string {
return "user:" + user
}
Expand All @@ -388,3 +429,26 @@ 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:")
}

func getRoleAssignmentsFromReadResponse(resp *fgaclient.ClientReadResponse, proj *string) []*minderv1.RoleAssignment {

Check failure on line 437 in internal/authz/authz.go

View workflow job for this annotation

GitHub Actions / lint / Go Lint

func `getRoleAssignmentsFromReadResponse` is unused (unused)
assignments := []*minderv1.RoleAssignment{}

for _, t := range resp.GetTuples() {
k := t.GetKey()
r, err := ParseRole(k.GetRelation())
if err != nil {
continue
}
assignments = append(assignments, &minderv1.RoleAssignment{
Subject: getUserFromTuple(k.GetUser()),
Role: r.String(),
Project: proj,
})
}

return assignments
}
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(ctx context.Context, sub string) ([]uuid.UUID, error) {

Check warning on line 68 in internal/authz/mock/noop_authz.go

View workflow job for this annotation

GitHub Actions / lint / Go Lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
return nil, nil
}

// PrepareForRun implements authz.Client
func (_ *NoopClient) PrepareForRun(_ context.Context) error {
return nil
Expand Down
5 changes: 5 additions & 0 deletions internal/authz/mock/simple_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func (_ *SimpleClient) AssignmentsToProject(_ context.Context, _ uuid.UUID) ([]*
return nil, nil
}

// ProjectsForUser implements authz.Client
func (_ *SimpleClient) ProjectsForUser(ctx context.Context, sub string) ([]uuid.UUID, error) {

Check warning on line 71 in internal/authz/mock/simple_authz.go

View workflow job for this annotation

GitHub Actions / lint / Go Lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
return nil, nil
}

// PrepareForRun implements authz.Client
func (_ *SimpleClient) PrepareForRun(_ context.Context) error {
return nil
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
5 changes: 0 additions & 5 deletions internal/controlplane/handlers_authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,6 @@ func TestEntityContextProjectInterceptor(t *testing.T) {
Return(db.User{
ID: 1,
}, nil)
store.EXPECT().
GetUserProjects(gomock.Any(), gomock.Any()).
Return([]db.GetUserProjectsRow{{
ID: defaultProjectID,
}}, nil)
},
expectedContext: engine.EntityContext{
// Uses the default project id
Expand Down
Loading

0 comments on commit 1d8255d

Please sign in to comment.