Skip to content

Commit

Permalink
Implement auth token issuance on cluster access (#179)
Browse files Browse the repository at this point in the history
* Implement auth token issuance on cluster access

Signed-off-by: Jan Steffen <[email protected]>

* Allow system admin access to all clusters

Signed-off-by: Jan Steffen <[email protected]>

* Add oncall for admins

Signed-off-by: Jan Steffen <[email protected]>
  • Loading branch information
jastBytes authored Jun 21, 2022
1 parent c1d4915 commit f98e55f
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 107 deletions.
16 changes: 15 additions & 1 deletion cmd/gateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,17 @@ var serverCmd = &cobra.Command{
return err
}

tenantClusterBindingRepository := repositories.NewTenantClusterBindingRepository(esr.NewInMemoryRepository[*projections.TenantClusterBinding]())
tenantClusterBindingProjector := projectors.NewTenantClusterBindingProjector()
tenantClusterBindingProjectingHandler := eventhandler.NewProjectingEventHandler[*projections.TenantClusterBinding](tenantClusterBindingProjector, tenantClusterBindingRepository)
tenantClusterBindingHandlerChain := eventsourcing.UseEventHandlerMiddleware(tenantClusterBindingProjectingHandler, eventhandler.NewEventStoreReplayMiddleware(esClient), eventhandler.NewEventStoreRefreshMiddleware(esClient, refreshDuration))
tenantClusterBindingMatcher := ebConsumer.Matcher().MatchAggregateType(aggregates.TenantClusterBinding)
if err := ebConsumer.AddHandler(ctx, tenantClusterBindingHandlerChain, tenantClusterBindingMatcher); err != nil {
return err
}

clusterAccessRepo := repositories.NewClusterAccessRepository(tenantClusterBindingRepository, clusterRepository, userRoleBindingRepository)

if err := handler.WarmUp(ctx, esClient, aggregates.User, userHandlerChain); err != nil {
return err
}
Expand All @@ -189,6 +200,9 @@ var serverCmd = &cobra.Command{
if err := handler.WarmUp(ctx, esClient, aggregates.Cluster, clusterHandlerChain); err != nil {
return err
}
if err := handler.WarmUp(ctx, esClient, aggregates.TenantClusterBinding, tenantClusterBindingHandlerChain); err != nil {
return err
}

// API servers
authServer, err := gateway.NewAuthServer(ctx, gatewayURL, server, policiesPath, userRoleBindingRepository)
Expand Down Expand Up @@ -223,7 +237,7 @@ var serverCmd = &cobra.Command{
}
tokenLifeTimePerRole[k] = k8sTokenValidityDuration
}
clusterAuthApiServer := gateway.NewClusterAuthAPIServer(gatewayURL, signer, clusterRepository, tokenLifeTimePerRole)
clusterAuthApiServer := gateway.NewClusterAuthAPIServer(gatewayURL, signer, clusterAccessRepo, tokenLifeTimePerRole)
apiTokenServer := gateway.NewAPITokenServer(gatewayURL, signer, userRepository)

authMiddleware := authm.NewAuthMiddleware(authServer.AsClient(), []string{
Expand Down
2 changes: 1 addition & 1 deletion go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ go-rebuild-mocks: .protobuf-deps $(MOCKGEN)
$(MOCKGEN) -copyright_file hack/copyright.lic -destination internal/test/api/gateway/gateway_auth_client.go github.com/finleap-connect/monoskope/pkg/api/gateway GatewayAuthClient
$(MOCKGEN) -copyright_file hack/copyright.lic -destination internal/test/eventsourcing/mock_handler.go github.com/finleap-connect/monoskope/pkg/eventsourcing EventHandler
$(MOCKGEN) -copyright_file hack/copyright.lic -destination internal/test/eventsourcing/aggregate_store.go github.com/finleap-connect/monoskope/pkg/eventsourcing AggregateStore
$(MOCKGEN) -copyright_file hack/copyright.lic -destination internal/test/domain/repositories/repositories.go github.com/finleap-connect/monoskope/pkg/domain/repositories UserRepository,ClusterRepository
$(MOCKGEN) -copyright_file hack/copyright.lic -destination internal/test/domain/repositories/repositories.go github.com/finleap-connect/monoskope/pkg/domain/repositories UserRepository,ClusterRepository,ClusterAccessRepository

go-run-: ## run cmd, e.g. `make go-run-gateway ARGS="server"` to pass arguments
$(GO) run cmd/$*/*.go $(ARGS)
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ require (
sigs.k8s.io/controller-runtime v0.11.2
)

require k8s.io/utils v0.0.0-20211116205334-6203023598ed

require (
cloud.google.com/go v0.99.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
Expand Down Expand Up @@ -77,7 +79,7 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
Expand Down Expand Up @@ -128,6 +130,7 @@ require (
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.10 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 // indirect
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect
Expand All @@ -137,7 +140,6 @@ require (
k8s.io/api v0.23.6 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
mellium.im/sasl v0.2.1 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
Expand Down
8 changes: 5 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down Expand Up @@ -1222,8 +1223,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -1530,8 +1531,9 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
11 changes: 5 additions & 6 deletions internal/gateway/auth_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,10 @@ import (

var _ = Describe("Gateway Auth Server", func() {
var (
ctx = context.Background()
expectedUserId = uuid.New()
expectedRole = roles.User
expectedScope = scopes.Tenant
expectedResource = "1234"
ctx = context.Background()
expectedUserId = uuid.New()
expectedRole = roles.User
expectedScope = scopes.Tenant
)

getTokenForUser := func(user *projections.User) string {
Expand All @@ -63,7 +62,7 @@ var _ = Describe("Gateway Auth Server", func() {
UserId: expectedUserId.String(),
Role: string(expectedRole),
Scope: string(expectedScope),
Resource: wrapperspb.String(expectedResource),
Resource: wrapperspb.String(testEnv.SomeTenantId.String()),
},
)
Expect(err).ToNot(HaveOccurred())
Expand Down
24 changes: 12 additions & 12 deletions internal/gateway/cluster_auth_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,32 @@ import (

type clusterAuthApiServer struct {
api.UnimplementedClusterAuthServer
log logger.Logger
signer jwt.JWTSigner
clusterRepo repositories.ClusterRepository
issuer string
validity map[string]time.Duration
log logger.Logger
signer jwt.JWTSigner
clusterAccessRepo repositories.ClusterAccessRepository
issuer string
validity map[string]time.Duration
}

func NewClusterAuthAPIServer(
issuer string,
signer jwt.JWTSigner,
clusterRepo repositories.ClusterRepository,
clusterAccessRepo repositories.ClusterAccessRepository,
validity map[string]time.Duration,
) api.ClusterAuthServer {
s := &clusterAuthApiServer{
log: logger.WithName("server"),
signer: signer,
clusterRepo: clusterRepo,
issuer: issuer,
validity: validity,
log: logger.WithName("server"),
signer: signer,
clusterAccessRepo: clusterAccessRepo,
issuer: issuer,
validity: validity,
}
return s
}

func (s *clusterAuthApiServer) GetAuthToken(ctx context.Context, request *api.ClusterAuthTokenRequest) (*api.ClusterAuthTokenResponse, error) {
response := new(api.ClusterAuthTokenResponse)
uc := usecases.NewGetAuthTokenUsecase(request, response, s.signer, s.clusterRepo, s.issuer, s.validity)
uc := usecases.NewGetAuthTokenUsecase(request, response, s.signer, s.clusterAccessRepo, s.issuer, s.validity)
err := uc.Run(ctx)
if err != nil {
return nil, errors.TranslateToGrpcError(err)
Expand Down
13 changes: 5 additions & 8 deletions internal/gateway/cluster_auth_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,22 @@ var _ = Describe("Internal/Gateway/ClusterAuthServer", func() {
mdManager, err := metadata.NewDomainMetadataManager(ctx)
Expect(err).ToNot(HaveOccurred())

It("can retrieve auth url", func() {
It("can request a cluster auth token", func() {
conn, err := CreateInsecureConnection(ctx, testEnv.ApiListenerAPIServer.Addr().String())
Expect(err).ToNot(HaveOccurred())

clusters, err := testEnv.ClusterRepo.AllWith(ctx, false)
Expect(err).ToNot(HaveOccurred())
Expect(len(clusters)).To(BeNumerically(">=", 1))
defer conn.Close()
apiClient := api.NewClusterAuthClient(conn)

mdManager.SetUserInformation(&metadata.UserInformation{
Id: uuid.MustParse(testEnv.AdminUser.GetId()),
Name: testEnv.AdminUser.Name,
Email: testEnv.AdminUser.Email,
Id: uuid.MustParse(testEnv.TenantAdminUser.GetId()),
Name: testEnv.TenantAdminUser.Name,
Email: testEnv.TenantAdminUser.Email,
NotBefore: time.Now().UTC(),
})

response, err := apiClient.GetAuthToken(mdManager.GetOutgoingGrpcContext(), &api.ClusterAuthTokenRequest{
ClusterId: clusters[0].Id,
ClusterId: testEnv.TestClusterId.String(),
Role: string(expectedRole),
})
Expect(err).ToNot(HaveOccurred())
Expand Down
24 changes: 20 additions & 4 deletions internal/gateway/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type TestEnv struct {
ExistingUser *projections.User
NotExistingUser *projections.User
PoliciesPath string
SomeTenantId uuid.UUID
TestClusterId uuid.UUID
}

func NewTestEnvWithParent(testeEnv *test.TestEnv) (*TestEnv, error) {
Expand Down Expand Up @@ -158,6 +160,8 @@ func NewTestEnvWithParent(testeEnv *test.TestEnv) (*TestEnv, error) {
return nil, err
}

env.SomeTenantId = uuid.New()

// Setup user repo
adminUser := projections.NewUserProjection(uuid.New())
adminUser.Name = "admin"
Expand All @@ -184,7 +188,7 @@ func NewTestEnvWithParent(testeEnv *test.TestEnv) (*TestEnv, error) {
tenantAdminRoleBinding.UserId = tenantAdminUser.Id
tenantAdminRoleBinding.Role = string(roles.Admin)
tenantAdminRoleBinding.Scope = string(scopes.Tenant)
tenantAdminRoleBinding.Resource = "1234"
tenantAdminRoleBinding.Resource = env.SomeTenantId.String()

env.AdminUser = adminUser
env.TenantAdminUser = tenantAdminUser
Expand Down Expand Up @@ -216,8 +220,8 @@ func NewTestEnvWithParent(testeEnv *test.TestEnv) (*TestEnv, error) {
}

// Setup cluster repo
clusterId := uuid.New()
testCluster := projections.NewClusterProjection(clusterId)
env.TestClusterId = uuid.New()
testCluster := projections.NewClusterProjection(env.TestClusterId)
testCluster.Name = "test-cluster"
testCluster.DisplayName = "Test Cluster"
testCluster.ApiServerAddress = "https://somecluster.io"
Expand All @@ -228,11 +232,23 @@ func NewTestEnvWithParent(testeEnv *test.TestEnv) (*TestEnv, error) {
return nil, err
}

tenantClusterBindingId := uuid.New()
tenantClusterBinding := projections.NewTenantClusterBindingProjection(tenantClusterBindingId)
tenantClusterBinding.ClusterId = env.TestClusterId.String()
tenantClusterBinding.TenantId = env.SomeTenantId.String()

inMemoryTemamtClusterBindingRepo := es_repos.NewInMemoryRepository[*projections.TenantClusterBinding]()
if err := inMemoryTemamtClusterBindingRepo.Upsert(context.Background(), tenantClusterBinding); err != nil {
return nil, err
}

userRoleBindingRepo := repositories.NewUserRoleBindingRepository(inMemoryUserRoleBindingRepo)
userRepo := repositories.NewUserRepository(inMemoryUserRepo, repositories.NewUserRoleBindingRepository(inMemoryUserRoleBindingRepo))
tenantClusterbindingRepo := repositories.NewTenantClusterBindingRepository(inMemoryTemamtClusterBindingRepo)
env.ClusterRepo = repositories.NewClusterRepository(inMemoryClusterRepo)

gatewayApiServer := NewGatewayAPIServer(env.ClientAuthConfig, authClient, authServer, userRepo)
authApiServer := NewClusterAuthAPIServer("https://localhost", signer, env.ClusterRepo, map[string]time.Duration{
authApiServer := NewClusterAuthAPIServer("https://localhost", signer, repositories.NewClusterAccessRepository(tenantClusterbindingRepo, env.ClusterRepo, userRoleBindingRepo), map[string]time.Duration{
"default": time.Hour * 1,
})

Expand Down
50 changes: 29 additions & 21 deletions internal/gateway/usecases/get_auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,35 @@ import (
"time"

"github.com/finleap-connect/monoskope/internal/gateway/auth"
"github.com/finleap-connect/monoskope/pkg/api/domain/projections"
api "github.com/finleap-connect/monoskope/pkg/api/gateway"
"github.com/finleap-connect/monoskope/pkg/domain/errors"
domainErrors "github.com/finleap-connect/monoskope/pkg/domain/errors"
"github.com/finleap-connect/monoskope/pkg/domain/metadata"
"github.com/finleap-connect/monoskope/pkg/domain/repositories"
"github.com/finleap-connect/monoskope/pkg/jwt"
"github.com/finleap-connect/monoskope/pkg/k8s"
"github.com/finleap-connect/monoskope/pkg/logger"
"github.com/finleap-connect/monoskope/pkg/usecase"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/utils/strings/slices"
)

type getAuthTokenUsecase struct {
*usecase.UseCaseBase
request *api.ClusterAuthTokenRequest
result *api.ClusterAuthTokenResponse
signer jwt.JWTSigner
clusterRepo repositories.ClusterRepository
issuer string
validity map[string]time.Duration
request *api.ClusterAuthTokenRequest
result *api.ClusterAuthTokenResponse
signer jwt.JWTSigner
clusterAccessRepo repositories.ClusterAccessRepository
issuer string
validity map[string]time.Duration
}

func NewGetAuthTokenUsecase(
request *api.ClusterAuthTokenRequest,
response *api.ClusterAuthTokenResponse,
signer jwt.JWTSigner,
clusterRepo repositories.ClusterRepository,
clusterAccessRepo repositories.ClusterAccessRepository,
issuer string,
validity map[string]time.Duration,
) usecase.UseCase {
Expand All @@ -56,7 +58,7 @@ func NewGetAuthTokenUsecase(
request,
response,
signer,
clusterRepo,
clusterAccessRepo,
issuer,
validity,
}
Expand All @@ -80,22 +82,28 @@ func (s *getAuthTokenUsecase) Run(ctx context.Context) error {
}

clusterId := s.request.GetClusterId()
s.Log.V(logger.DebugLevel).Info("Getting cluster by id...", "id", clusterId)

uuid, err := uuid.Parse(clusterId)
s.Log.V(logger.DebugLevel).Info("Checking user is allowed to access cluster...", "clusterId", clusterId)
clusterAccesses, err := s.clusterAccessRepo.GetClustersAccessibleByUserId(ctx, userInfo.Id)
if err != nil {
return err
}

cluster, err := s.clusterRepo.ById(ctx, uuid)
if err != nil {
return err
var foundClusterAccess *projections.ClusterAccess
for _, clusterAccess := range clusterAccesses {
if clusterAccess.Cluster.Id == clusterId {
foundClusterAccess = clusterAccess
break
}
}

k8sRole := s.request.GetRole()
s.Log.V(logger.DebugLevel).Info("Validating role exists...", "role", k8sRole)
if err := k8s.ValidateRole(k8sRole); err != nil {
return err
if foundClusterAccess == nil {
s.Log.V(logger.DebugLevel).Info("User is not authorized to access cluster.", "clusterId", clusterId)
return errors.ErrUnauthorized
}

if !slices.Contains(foundClusterAccess.Roles, s.request.Role) {
s.Log.V(logger.DebugLevel).Info("User is not authorized to access cluster with role.", "clusterId", clusterId, "role", s.request.Role)
return errors.ErrUnauthorized
}

username := strings.ToLower(strings.Split(userInfo.Email, "@")[0])
Expand All @@ -109,8 +117,8 @@ func (s *getAuthTokenUsecase) Run(ctx context.Context) error {
Email: userInfo.Email,
EmailVerified: true,
}, &jwt.ClusterClaim{
ClusterId: cluster.GetId(),
ClusterName: cluster.GetName(),
ClusterId: foundClusterAccess.Cluster.GetId(),
ClusterName: foundClusterAccess.Cluster.GetName(),
ClusterUserName: username,
ClusterRole: s.request.Role,
}, s.issuer, userInfo.Id.String(), s.validity[s.request.Role])
Expand Down
Loading

0 comments on commit f98e55f

Please sign in to comment.