Skip to content

Commit

Permalink
Merge pull request #172 from uselagoon/configurable-rbac
Browse files Browse the repository at this point in the history
Add flag to block Developer SSH access
  • Loading branch information
smlx authored Feb 15, 2023
2 parents 5807869 + aa936b3 commit a3fbdb6
Show file tree
Hide file tree
Showing 11 changed files with 480 additions and 231 deletions.
1 change: 1 addition & 0 deletions cmd/ssh-portal-api/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main is the executable ssh-portal-api service.
package main

import (
Expand Down
11 changes: 10 additions & 1 deletion cmd/ssh-portal-api/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/uselagoon/ssh-portal/internal/keycloak"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"github.com/uselagoon/ssh-portal/internal/metrics"
"github.com/uselagoon/ssh-portal/internal/rbac"
"github.com/uselagoon/ssh-portal/internal/sshportalapi"
"go.uber.org/zap"
)
Expand All @@ -20,6 +21,7 @@ type ServeCmd struct {
APIDBDatabase string `kong:"default='infrastructure',env='API_DB_DATABASE',help='Lagoon API DB Database Name'"`
APIDBPassword string `kong:"required,env='API_DB_PASSWORD',help='Lagoon API DB Password'"`
APIDBUsername string `kong:"default='api',env='API_DB_USERNAME',help='Lagoon API DB Username'"`
BlockDeveloperSSH bool `kong:"env='BLOCK_DEVELOPER_SSH',help='Disallow Developer SSH access'"`
KeycloakBaseURL string `kong:"required,env='KEYCLOAK_BASE_URL',help='Keycloak Base URL'"`
KeycloakClientID string `kong:"default='service-api',env='KEYCLOAK_SERVICE_API_CLIENT_ID',help='Keycloak OAuth2 Client ID'"`
KeycloakClientSecret string `kong:"required,env='KEYCLOAK_SERVICE_API_CLIENT_SECRET',help='Keycloak OAuth2 Client Secret'"`
Expand All @@ -35,6 +37,13 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
// get main process context, which cancels on SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer stop()
// init RBAC permission engine
var p *rbac.Permission
if cmd.BlockDeveloperSSH {
p = rbac.NewPermission(rbac.BlockDeveloperSSH())
} else {
p = rbac.NewPermission()
}
// init lagoon DB client
dbConf := mysql.NewConfig()
dbConf.Addr = cmd.APIDBAddress
Expand All @@ -53,5 +62,5 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
return fmt.Errorf("couldn't init keycloak Client: %v", err)
}
// start serving NATS requests
return sshportalapi.ServeNATS(ctx, stop, log, l, k, cmd.NATSURL)
return sshportalapi.ServeNATS(ctx, stop, log, p, l, k, cmd.NATSURL)
}
21 changes: 15 additions & 6 deletions cmd/ssh-token/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/uselagoon/ssh-portal/internal/keycloak"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"github.com/uselagoon/ssh-portal/internal/metrics"
"github.com/uselagoon/ssh-portal/internal/rbac"
"github.com/uselagoon/ssh-portal/internal/sshtoken"
"go.uber.org/zap"
)
Expand All @@ -21,15 +22,16 @@ type ServeCmd struct {
APIDBDatabase string `kong:"default='infrastructure',env='API_DB_DATABASE',help='Lagoon API DB Database Name'"`
APIDBPassword string `kong:"required,env='API_DB_PASSWORD',help='Lagoon API DB Password'"`
APIDBUsername string `kong:"default='api',env='API_DB_USERNAME',help='Lagoon API DB Username'"`
BlockDeveloperSSH bool `kong:"env='BLOCK_DEVELOPER_SSH',help='Disallow Developer SSH access'"`
HostKeyECDSA string `kong:"env='HOST_KEY_ECDSA',help='PEM encoded ECDSA host key'"`
HostKeyED25519 string `kong:"env='HOST_KEY_ED25519',help='PEM encoded Ed25519 host key'"`
HostKeyRSA string `kong:"env='HOST_KEY_RSA',help='PEM encoded RSA host key'"`
KeycloakBaseURL string `kong:"required,env='KEYCLOAK_BASE_URL',help='Keycloak Base URL'"`
KeycloakTokenClientID string `kong:"default='auth-server',env='KEYCLOAK_AUTH_SERVER_CLIENT_ID',help='Keycloak auth-server OAuth2 Client ID'"`
KeycloakTokenClientSecret string `kong:"required,env='KEYCLOAK_AUTH_SERVER_CLIENT_SECRET',help='Keycloak auth-server OAuth2 Client Secret'"`
KeycloakPermissionClientID string `kong:"default='service-api',env='KEYCLOAK_SERVICE_API_CLIENT_ID',help='Keycloak service-api OAuth2 Client ID'"`
KeycloakPermissionClientSecret string `kong:"env='KEYCLOAK_SERVICE_API_CLIENT_SECRET',help='Keycloak service-api OAuth2 Client Secret'"`
KeycloakTokenClientID string `kong:"default='auth-server',env='KEYCLOAK_AUTH_SERVER_CLIENT_ID',help='Keycloak auth-server OAuth2 Client ID'"`
KeycloakTokenClientSecret string `kong:"required,env='KEYCLOAK_AUTH_SERVER_CLIENT_SECRET',help='Keycloak auth-server OAuth2 Client Secret'"`
SSHServerPort uint `kong:"default='2222',env='SSH_SERVER_PORT',help='Port the SSH server will listen on for SSH client connections'"`
HostKeyECDSA string `kong:"env='HOST_KEY_ECDSA',help='PEM encoded ECDSA host key'"`
HostKeyED25519 string `kong:"env='HOST_KEY_ED25519',help='PEM encoded Ed25519 host key'"`
HostKeyRSA string `kong:"env='HOST_KEY_RSA',help='PEM encoded RSA host key'"`
}

// Run the serve command to ssh-portal API requests.
Expand All @@ -41,6 +43,13 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
// get main process context, which cancels on SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer stop()
// init RBAC permission engine
var p *rbac.Permission
if cmd.BlockDeveloperSSH {
p = rbac.NewPermission(rbac.BlockDeveloperSSH())
} else {
p = rbac.NewPermission()
}
// init lagoon DB client
dbConf := mysql.NewConfig()
dbConf.Addr = cmd.APIDBAddress
Expand Down Expand Up @@ -78,6 +87,6 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
}
}
// start serving SSH token requests
return sshtoken.Serve(ctx, log, l, ldb, keycloakToken, keycloakPermission,
return sshtoken.Serve(ctx, log, l, p, ldb, keycloakToken, keycloakPermission,
hostkeys)
}
185 changes: 0 additions & 185 deletions internal/permission/usercansshtoenvironment_test.go

This file was deleted.

44 changes: 44 additions & 0 deletions internal/rbac/permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Package rbac contains permission logic for Lagoon.
package rbac

import "github.com/uselagoon/ssh-portal/internal/lagoon"

// Permission encapsulates the permission logic for Lagoon.
// This object should not be constructed by itself, only via NewPermission().
type Permission struct {
envTypeRoleCanSSH map[lagoon.EnvironmentType][]lagoon.UserRole
}

// Option performs optional configuration on Permission objects during
// initialization, and is passed to NewPermission().
type Option func(*Permission)

// BlockDeveloperSSH configures the Permission object returned by
// NewPermission() to disallow Developer SSH access to Lagoon environments.
// Instead, only Maintainers and Owners can SSH to either Development or
// Production environments.
func BlockDeveloperSSH() Option {
return func(p *Permission) {
p.envTypeRoleCanSSH = map[lagoon.EnvironmentType][]lagoon.UserRole{
lagoon.Development: {
lagoon.Maintainer,
lagoon.Owner,
},
lagoon.Production: {
lagoon.Maintainer,
lagoon.Owner,
},
}
}
}

// NewPermission applies the given Options and returns a new Permission object.
func NewPermission(opts ...Option) *Permission {
p := Permission{
envTypeRoleCanSSH: defaultEnvTypeRoleCanSSH,
}
for _, opt := range opts {
opt(&p)
}
return &p
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package permission
package rbac

import (
"context"
Expand All @@ -9,10 +9,20 @@ import (
"go.opentelemetry.io/otel"
)

const pkgName = "github.com/uselagoon/ssh-portal/internal/permission"
const pkgName = "github.com/uselagoon/ssh-portal/internal/rbac"

// map environment type to role which can SSH
var envTypeRoleCanSSH = map[lagoon.EnvironmentType][]lagoon.UserRole{
// Default permission map of environment type to roles which can SSH.
//
// By default:
// - Developer and higher can SSH to development environments.
// - Maintainer and higher can SSH to production environments.
//
// See https://docs.lagoon.sh/administering-lagoon/rbac/#group-roles for more
// information.
//
// Note that this does not affect the platform-owner role, which can always SSH
// to any environment.
var defaultEnvTypeRoleCanSSH = map[lagoon.EnvironmentType][]lagoon.UserRole{
lagoon.Development: {
lagoon.Developer,
lagoon.Maintainer,
Expand All @@ -27,7 +37,7 @@ var envTypeRoleCanSSH = map[lagoon.EnvironmentType][]lagoon.UserRole{
// UserCanSSHToEnvironment returns true if the given environment can be
// connected to via SSH by the user with the given realm roles and user groups,
// and false otherwise.
func UserCanSSHToEnvironment(ctx context.Context, env *lagoondb.Environment,
func (p *Permission) UserCanSSHToEnvironment(ctx context.Context, env *lagoondb.Environment,
realmRoles, userGroups []string, groupProjectIDs map[string][]int) bool {
// set up tracing
_, span := otel.Tracer(pkgName).Start(ctx, "UserCanSSHToEnvironment")
Expand All @@ -38,7 +48,7 @@ func UserCanSSHToEnvironment(ctx context.Context, env *lagoondb.Environment,
return true
}
}
validRoles := envTypeRoleCanSSH[env.Type]
validRoles := p.envTypeRoleCanSSH[env.Type]
// check if the user is directly a member of the project group and has the
// required role
var validProjectGroups []string
Expand Down
Loading

0 comments on commit a3fbdb6

Please sign in to comment.