Introduce "compute:connect" role

This commit is contained in:
Nikolay Edigaryev 2026-01-29 17:54:19 +01:00
parent 688238837a
commit 5d64ff08b8
5 changed files with 73 additions and 25 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/go-openapi/runtime/middleware"
"github.com/penglongli/gin-metrics/ginmetrics"
"github.com/samber/lo"
"go.uber.org/zap"
"google.golang.org/grpc/metadata"
)
@ -239,9 +240,31 @@ func (controller *Controller) authenticateMiddleware(c *gin.Context) {
c.Next()
}
type AuthorizeMode int
const (
AuthorizeModeAll AuthorizeMode = iota
AuthorizeModeAny
)
func (controller *Controller) authorize(
ctx *gin.Context,
requiredRoles ...v1pkg.ServiceAccountRole,
) responder.Responder {
return controller.authorizeBase(ctx, AuthorizeModeAll, requiredRoles...)
}
func (controller *Controller) authorizeAny(
ctx *gin.Context,
requiredRoles ...v1pkg.ServiceAccountRole,
) responder.Responder {
return controller.authorizeBase(ctx, AuthorizeModeAny, requiredRoles...)
}
func (controller *Controller) authorizeBase(
ctx *gin.Context,
mode AuthorizeMode,
requiredRoles ...v1pkg.ServiceAccountRole,
) responder.Responder {
if controller.insecureAuthDisabled {
return nil
@ -254,21 +277,34 @@ func (controller *Controller) authorize(
serviceAccount := serviceAccountUntyped.(*v1pkg.ServiceAccount)
serviceAccountRolesSet := mapset.NewSet[v1pkg.ServiceAccountRole](serviceAccount.Roles...)
requiredRolesSet := mapset.NewSet[v1pkg.ServiceAccountRole](requiredRoles...)
var authorized bool
missingRoles := requiredRolesSet.Difference(serviceAccountRolesSet).ToSlice()
if len(missingRoles) == 0 {
switch mode {
case AuthorizeModeAll:
authorized = serviceAccountRolesSet.Contains(requiredRoles...)
case AuthorizeModeAny:
authorized = serviceAccountRolesSet.ContainsAny(requiredRoles...)
}
if authorized {
return nil
}
var missingRolesStrings []string
var hint string
for _, missingRole := range missingRoles {
missingRolesStrings = append(missingRolesStrings, string(missingRole))
switch mode {
case AuthorizeModeAll:
hint = "all of the following roles must be present"
case AuthorizeModeAny:
hint = "any of the following roles must be present"
}
humanizedRoles := lo.Map(requiredRoles, func(role v1pkg.ServiceAccountRole, _ int) string {
return string(role)
})
return responder.JSON(http.StatusUnauthorized,
NewErrorResponse("missing roles: %s", strings.Join(missingRolesStrings, ", ")))
NewErrorResponse("%s: %s", hint, strings.Join(humanizedRoles, ", ")))
}
func (controller *Controller) authorizeGRPC(ctx context.Context, scopes ...v1pkg.ServiceAccountRole) bool {

View File

@ -3,18 +3,20 @@ package controller
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/cirruslabs/orchard/internal/responder"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/cirruslabs/orchard/rpc"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"net/http"
"strconv"
"time"
)
func (controller *Controller) ip(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite,
v1.ServiceAccountRoleComputeConnect); responder != nil {
return responder
}

View File

@ -3,6 +3,10 @@ package controller
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/internal/netconncancel"
"github.com/cirruslabs/orchard/internal/proxy"
@ -15,13 +19,11 @@ import (
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
"strconv"
"time"
)
func (controller *Controller) portForwardVM(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite,
v1.ServiceAccountRoleComputeConnect); responder != nil {
return responder
}

View File

@ -5,6 +5,10 @@ import (
"crypto/subtle"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/cirruslabs/orchard/internal/controller/notifier"
"github.com/cirruslabs/orchard/internal/controller/rendezvous"
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
@ -15,9 +19,6 @@ import (
"github.com/samber/lo"
"go.uber.org/zap"
"golang.org/x/crypto/ssh"
"net"
"strings"
"time"
)
const (
@ -110,9 +111,12 @@ func (server *SSHServer) passwordCallback(connMetadata ssh.ConnMetadata, passwor
}
// Authorize
if !lo.Contains(serviceAccount.Roles, v1.ServiceAccountRoleComputeWrite) {
return fmt.Errorf("authorization failed for user %q because it lacks %q role",
connMetadata.User(), v1.ServiceAccountRoleComputeWrite)
authorized := lo.Contains(serviceAccount.Roles, v1.ServiceAccountRoleComputeWrite) ||
lo.Contains(serviceAccount.Roles, v1.ServiceAccountRoleComputeConnect)
if !authorized {
return fmt.Errorf("authorization failed for user %q because it lacks %q or %q roles",
connMetadata.User(), v1.ServiceAccountRoleComputeWrite, v1.ServiceAccountRoleComputeConnect)
}
return nil

View File

@ -10,10 +10,11 @@ var ErrUnsupportedServiceAccountRole = errors.New("unsupported service account r
type ServiceAccountRole string
const (
ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read"
ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write"
ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read"
ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write"
ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read"
ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write"
ServiceAccountRoleComputeConnect ServiceAccountRole = "compute:connect"
ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read"
ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write"
)
func NewServiceAccountRole(name string) (ServiceAccountRole, error) {
@ -22,6 +23,8 @@ func NewServiceAccountRole(name string) (ServiceAccountRole, error) {
return ServiceAccountRoleComputeRead, nil
case string(ServiceAccountRoleComputeWrite):
return ServiceAccountRoleComputeWrite, nil
case string(ServiceAccountRoleComputeConnect):
return ServiceAccountRoleComputeConnect, nil
case string(ServiceAccountRoleAdminRead):
return ServiceAccountRoleAdminRead, nil
case string(ServiceAccountRoleAdminWrite):
@ -35,6 +38,7 @@ func AllServiceAccountRoles() []ServiceAccountRole {
return []ServiceAccountRole{
ServiceAccountRoleComputeRead,
ServiceAccountRoleComputeWrite,
ServiceAccountRoleComputeConnect,
ServiceAccountRoleAdminRead,
ServiceAccountRoleAdminWrite,
}