Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ func buildAPIDependencies(
groupService := group.NewService(groupRepository, relationService, authnService, policyService)
organizationService := organization.NewService(organizationRepository, relationService, userService,
authnService, policyService, preferenceService, roleService)
authnService.SetOrgService(organizationService)
projectRepository := postgres.NewProjectRepository(dbc)
projectService := project.NewService(projectRepository, relationService, userService, policyService,
authnService, serviceUserService, groupService, roleService)
Expand Down
31 changes: 31 additions & 0 deletions core/authenticate/authenticators.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,37 @@ func authenticateWithSession(ctx context.Context, s *Service) (Principal, error)
return Principal{}, errSkip
}

func (s *Service) ensurePrincipalOrgEnabled(ctx context.Context, p Principal) error {
switch p.Type {
case schema.PATPrincipal:
if p.PAT != nil {
return s.ensureOrgEnabled(ctx, p.PAT.OrgID)
}
case schema.ServiceUserPrincipal:
if p.ServiceUser != nil {
return s.ensureOrgEnabled(ctx, p.ServiceUser.OrgID)
}
}
return nil
}

func (s *Service) ensureOrgEnabled(ctx context.Context, orgID string) error {
if s.orgService == nil || orgID == schema.PlatformOrgID.String() {
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if orgID == "" {
return errors.ErrForbidden
}
enabled, err := s.orgService.IsEnabled(ctx, orgID)
if err != nil {
return err
}
if !enabled {
return errors.ErrForbidden
}
return nil
}

// authenticateWithPAT validates a personal access token.
func authenticateWithPAT(ctx context.Context, s *Service) (Principal, error) {
value, ok := GetTokenFromContext(ctx)
Expand Down
93 changes: 93 additions & 0 deletions core/authenticate/mocks/org_service.go

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

12 changes: 12 additions & 0 deletions core/authenticate/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ type UserPATService interface {
GetByID(ctx context.Context, id string) (patModels.PAT, error)
}

type OrgService interface {
IsEnabled(ctx context.Context, orgID string) (bool, error)
}

type Service struct {
log *slog.Logger
cron *cron.Cron
Expand All @@ -107,6 +111,7 @@ type Service struct {
sessionService SessionService
serviceUserService ServiceUserService
userPATService UserPATService
orgService OrgService
webAuth *webauthn.WebAuthn
}

Expand Down Expand Up @@ -136,6 +141,10 @@ func NewService(logger *slog.Logger, config Config, flowRepo FlowRepository,
return r
}

func (s *Service) SetOrgService(orgService OrgService) {
s.orgService = orgService
}

func (s Service) SupportedStrategies() []string {
// add here strategies like mail link once implemented
var strategies []string
Expand Down Expand Up @@ -798,6 +807,9 @@ func (s Service) GetPrincipal(ctx context.Context, assertions ...ClientAssertion
principal, err := authenticator(ctx, &s)
if err == nil {
principal.AuthVia = assertion
if err := s.ensurePrincipalOrgEnabled(ctx, principal); err != nil {
return Principal{}, err
}
return principal, nil
}
if !errors.Is(err, errSkip) {
Expand Down
133 changes: 133 additions & 0 deletions core/authenticate/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/raystack/frontier/core/user"
patModels "github.com/raystack/frontier/core/userpat/models"
"github.com/raystack/frontier/internal/bootstrap/schema"
frontiererrors "github.com/raystack/frontier/pkg/errors"
mailerMock "github.com/raystack/frontier/pkg/mailer/mocks"
pkgMetadata "github.com/raystack/frontier/pkg/metadata"
"github.com/raystack/frontier/pkg/server/consts"
Expand Down Expand Up @@ -510,3 +511,135 @@ func TestService_GetPrincipal_RestrictsByAuthVia(t *testing.T) {
})
}
}

func TestService_GetPrincipal_OrgStateGate(t *testing.T) {
orgID := uuid.New().String()
userID := uuid.New().String()
patID := uuid.New().String()
secret := base64.StdEncoding.EncodeToString([]byte("user:password"))

tests := []struct {
name string
ctx context.Context
assertions []authenticate.ClientAssertion
want authenticate.Principal
wantErr error
setup func() *authenticate.Service
}{
{
name: "reject PAT whose org is disabled or gone",
ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{
consts.UserTokenGatewayKey: {"pat-token"},
}),
assertions: []authenticate.ClientAssertion{authenticate.PATClientAssertion},
wantErr: frontiererrors.ErrForbidden,
setup: func() *authenticate.Service {
pat := mocks.NewUserPATService(t)
pat.EXPECT().Validate(mock.Anything, "pat-token").Return(patModels.PAT{ID: patID, UserID: userID, OrgID: orgID}, nil)
usr := mocks.NewUserService(t)
usr.EXPECT().GetByID(mock.Anything, userID).Return(user.User{ID: userID}, nil)
org := mocks.NewOrgService(t)
org.EXPECT().IsEnabled(mock.Anything, orgID).Return(false, nil)
s := authenticate.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), authenticate.Config{},
nil, nil, nil, nil, usr, nil, nil, pat)
s.SetOrgService(org)
return s
},
},
{
name: "reject PAT with empty org id",
ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{
consts.UserTokenGatewayKey: {"pat-token"},
}),
assertions: []authenticate.ClientAssertion{authenticate.PATClientAssertion},
wantErr: frontiererrors.ErrForbidden,
setup: func() *authenticate.Service {
pat := mocks.NewUserPATService(t)
pat.EXPECT().Validate(mock.Anything, "pat-token").Return(patModels.PAT{ID: patID, UserID: userID, OrgID: ""}, nil)
usr := mocks.NewUserService(t)
usr.EXPECT().GetByID(mock.Anything, userID).Return(user.User{ID: userID}, nil)
org := mocks.NewOrgService(t)
s := authenticate.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), authenticate.Config{},
nil, nil, nil, nil, usr, nil, nil, pat)
s.SetOrgService(org)
return s
},
},
{
name: "allow PAT whose org is enabled",
ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{
consts.UserTokenGatewayKey: {"pat-token"},
}),
assertions: []authenticate.ClientAssertion{authenticate.PATClientAssertion},
want: authenticate.Principal{
ID: patID,
Type: schema.PATPrincipal,
AuthVia: authenticate.PATClientAssertion,
PAT: &patModels.PAT{ID: patID, UserID: userID, OrgID: orgID},
User: &user.User{ID: userID},
},
setup: func() *authenticate.Service {
pat := mocks.NewUserPATService(t)
pat.EXPECT().Validate(mock.Anything, "pat-token").Return(patModels.PAT{ID: patID, UserID: userID, OrgID: orgID}, nil)
usr := mocks.NewUserService(t)
usr.EXPECT().GetByID(mock.Anything, userID).Return(user.User{ID: userID}, nil)
org := mocks.NewOrgService(t)
org.EXPECT().IsEnabled(mock.Anything, orgID).Return(true, nil)
s := authenticate.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), authenticate.Config{},
nil, nil, nil, nil, usr, nil, nil, pat)
s.SetOrgService(org)
return s
},
},
{
name: "reject service user (client credentials) whose org is disabled or gone",
ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{
consts.UserSecretGatewayKey: {secret},
}),
assertions: []authenticate.ClientAssertion{authenticate.ClientCredentialsClientAssertion},
wantErr: frontiererrors.ErrForbidden,
setup: func() *authenticate.Service {
su := mocks.NewServiceUserService(t)
su.EXPECT().GetBySecret(mock.Anything, "user", "password").Return(serviceuser.ServiceUser{ID: userID, OrgID: orgID}, nil)
org := mocks.NewOrgService(t)
org.EXPECT().IsEnabled(mock.Anything, orgID).Return(false, nil)
s := authenticate.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), authenticate.Config{},
nil, nil, nil, nil, nil, su, nil, nil)
s.SetOrgService(org)
return s
},
},
{
name: "reject service user (jwt grant) whose org is disabled or gone",
ctx: metadata.NewIncomingContext(context.Background(), map[string][]string{
consts.UserTokenGatewayKey: {"grant-token"},
}),
assertions: []authenticate.ClientAssertion{authenticate.JWTGrantClientAssertion},
wantErr: frontiererrors.ErrForbidden,
setup: func() *authenticate.Service {
su := mocks.NewServiceUserService(t)
su.EXPECT().GetByJWT(mock.Anything, "grant-token").Return(serviceuser.ServiceUser{ID: userID, OrgID: orgID}, nil)
org := mocks.NewOrgService(t)
org.EXPECT().IsEnabled(mock.Anything, orgID).Return(false, nil)
s := authenticate.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), authenticate.Config{},
nil, nil, nil, nil, nil, su, nil, nil)
s.SetOrgService(org)
return s
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := tt.setup()
got, err := s.GetPrincipal(tt.ctx, tt.assertions...)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
return
}
assert.NoError(t, err)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("GetPrincipal() mismatch (-want +got):\n%s", diff)
}
})
}
}
12 changes: 12 additions & 0 deletions core/organization/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ func (s Service) Get(ctx context.Context, idOrName string) (Organization, error)
return orgResp, nil
}

func (s Service) IsEnabled(ctx context.Context, id string) (bool, error) {
_, err := s.Get(ctx, id)
switch {
case err == nil:
return true, nil
case errors.Is(err, ErrDisabled), errors.Is(err, ErrNotExist):
return false, nil
default:
return false, err
}
}

func (s Service) GetByIDs(ctx context.Context, ids []string) ([]Organization, error) {
return s.repository.GetByIDs(ctx, ids)
}
Expand Down
47 changes: 47 additions & 0 deletions core/organization/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,53 @@ func TestService_Get(t *testing.T) {
})
}

func TestService_IsEnabled(t *testing.T) {
newSvc := func(t *testing.T) (*mocks.Repository, *organization.Service) {
t.Helper()
mockRepo := mocks.NewRepository(t)
svc := organization.NewService(mockRepo, mocks.NewRelationService(t), mocks.NewUserService(t),
mocks.NewAuthnService(t), mocks.NewPolicyService(t), mocks.NewPreferencesService(t), mocks.NewRoleService(t))
return mockRepo, svc
}

t.Run("enabled org returns true", func(t *testing.T) {
mockRepo, svc := newSvc(t)
id := uuid.New().String()
mockRepo.On("GetByID", mock.Anything, id).Return(organization.Organization{ID: id, State: organization.Enabled}, nil).Once()
ok, err := svc.IsEnabled(context.Background(), id)
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("disabled org returns false", func(t *testing.T) {
mockRepo, svc := newSvc(t)
id := uuid.New().String()
mockRepo.On("GetByID", mock.Anything, id).Return(organization.Organization{ID: id, State: organization.Disabled}, nil).Once()
ok, err := svc.IsEnabled(context.Background(), id)
assert.NoError(t, err)
assert.False(t, ok)
})

t.Run("missing org returns false", func(t *testing.T) {
mockRepo, svc := newSvc(t)
id := uuid.New().String()
mockRepo.On("GetByID", mock.Anything, id).Return(organization.Organization{}, organization.ErrNotExist).Once()
ok, err := svc.IsEnabled(context.Background(), id)
assert.NoError(t, err)
assert.False(t, ok)
})

t.Run("unexpected error propagates", func(t *testing.T) {
mockRepo, svc := newSvc(t)
id := uuid.New().String()
repoErr := errors.New("db down")
mockRepo.On("GetByID", mock.Anything, id).Return(organization.Organization{}, repoErr).Once()
ok, err := svc.IsEnabled(context.Background(), id)
assert.ErrorIs(t, err, repoErr)
assert.False(t, ok)
})
}

func TestService_GetRaw(t *testing.T) {
mockRepo := mocks.NewRepository(t)
mockRelationSvc := mocks.NewRelationService(t)
Expand Down
Loading
Loading