Skip to content

Commit 2c0629f

Browse files
authored
Introduce "compute:connect" role (#393)
* Introduce "compute:connect" role * Fix message fixture in TestAuthorizeAuthenticatedNoRoles
1 parent 6882388 commit 2c0629f

6 files changed

Lines changed: 77 additions & 28 deletions

File tree

internal/controller/api.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/gin-gonic/gin"
1919
"github.com/go-openapi/runtime/middleware"
2020
"github.com/penglongli/gin-metrics/ginmetrics"
21+
"github.com/samber/lo"
2122
"go.uber.org/zap"
2223
"google.golang.org/grpc/metadata"
2324
)
@@ -239,9 +240,31 @@ func (controller *Controller) authenticateMiddleware(c *gin.Context) {
239240
c.Next()
240241
}
241242

243+
type AuthorizeMode int
244+
245+
const (
246+
AuthorizeModeAll AuthorizeMode = iota
247+
AuthorizeModeAny
248+
)
249+
242250
func (controller *Controller) authorize(
243251
ctx *gin.Context,
244252
requiredRoles ...v1pkg.ServiceAccountRole,
253+
) responder.Responder {
254+
return controller.authorizeBase(ctx, AuthorizeModeAll, requiredRoles...)
255+
}
256+
257+
func (controller *Controller) authorizeAny(
258+
ctx *gin.Context,
259+
requiredRoles ...v1pkg.ServiceAccountRole,
260+
) responder.Responder {
261+
return controller.authorizeBase(ctx, AuthorizeModeAny, requiredRoles...)
262+
}
263+
264+
func (controller *Controller) authorizeBase(
265+
ctx *gin.Context,
266+
mode AuthorizeMode,
267+
requiredRoles ...v1pkg.ServiceAccountRole,
245268
) responder.Responder {
246269
if controller.insecureAuthDisabled {
247270
return nil
@@ -254,21 +277,34 @@ func (controller *Controller) authorize(
254277
serviceAccount := serviceAccountUntyped.(*v1pkg.ServiceAccount)
255278
serviceAccountRolesSet := mapset.NewSet[v1pkg.ServiceAccountRole](serviceAccount.Roles...)
256279

257-
requiredRolesSet := mapset.NewSet[v1pkg.ServiceAccountRole](requiredRoles...)
280+
var authorized bool
258281

259-
missingRoles := requiredRolesSet.Difference(serviceAccountRolesSet).ToSlice()
260-
if len(missingRoles) == 0 {
282+
switch mode {
283+
case AuthorizeModeAll:
284+
authorized = serviceAccountRolesSet.Contains(requiredRoles...)
285+
case AuthorizeModeAny:
286+
authorized = serviceAccountRolesSet.ContainsAny(requiredRoles...)
287+
}
288+
289+
if authorized {
261290
return nil
262291
}
263292

264-
var missingRolesStrings []string
293+
var hint string
265294

266-
for _, missingRole := range missingRoles {
267-
missingRolesStrings = append(missingRolesStrings, string(missingRole))
295+
switch mode {
296+
case AuthorizeModeAll:
297+
hint = "all of the following roles must be present"
298+
case AuthorizeModeAny:
299+
hint = "any of the following roles must be present"
268300
}
269301

302+
humanizedRoles := lo.Map(requiredRoles, func(role v1pkg.ServiceAccountRole, _ int) string {
303+
return string(role)
304+
})
305+
270306
return responder.JSON(http.StatusUnauthorized,
271-
NewErrorResponse("missing roles: %s", strings.Join(missingRolesStrings, ", ")))
307+
NewErrorResponse("%s: %s", hint, strings.Join(humanizedRoles, ", ")))
272308
}
273309

274310
func (controller *Controller) authorizeGRPC(ctx context.Context, scopes ...v1pkg.ServiceAccountRole) bool {

internal/controller/api_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
package controller
33

44
import (
5+
"net/http"
6+
"testing"
7+
58
"github.com/cirruslabs/orchard/internal/responder"
69
v1pkg "github.com/cirruslabs/orchard/pkg/resource/v1"
710
"github.com/gin-gonic/gin"
811
"github.com/stretchr/testify/require"
9-
"net/http"
10-
"testing"
1112
)
1213

1314
func TestAuthorizeInsecureAuthDisabled(t *testing.T) {
@@ -31,7 +32,7 @@ func TestAuthorizeAuthenticatedNoRoles(t *testing.T) {
3132

3233
const requiredRole = v1pkg.ServiceAccountRoleAdminWrite
3334

34-
require.Equal(t, responder.JSON(http.StatusUnauthorized, NewErrorResponse("missing roles: %s", requiredRole)),
35+
require.Equal(t, responder.JSON(http.StatusUnauthorized, NewErrorResponse("all of the following roles must be present: %s", requiredRole)),
3536
controller.authorize(ctx, requiredRole))
3637
}
3738

internal/controller/api_vms_ip.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ package controller
33
import (
44
"context"
55
"fmt"
6+
"net/http"
7+
"strconv"
8+
"time"
9+
610
"github.com/cirruslabs/orchard/internal/responder"
711
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
812
"github.com/cirruslabs/orchard/rpc"
913
"github.com/gin-gonic/gin"
1014
"github.com/google/uuid"
11-
"net/http"
12-
"strconv"
13-
"time"
1415
)
1516

1617
func (controller *Controller) ip(ctx *gin.Context) responder.Responder {
17-
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
18+
if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite,
19+
v1.ServiceAccountRoleComputeConnect); responder != nil {
1820
return responder
1921
}
2022

internal/controller/api_vms_portforward.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package controller
33
import (
44
"context"
55
"fmt"
6+
"net/http"
7+
"strconv"
8+
"time"
9+
610
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
711
"github.com/cirruslabs/orchard/internal/netconncancel"
812
"github.com/cirruslabs/orchard/internal/proxy"
@@ -15,13 +19,11 @@ import (
1519
"github.com/pkg/errors"
1620
"google.golang.org/grpc/codes"
1721
"google.golang.org/grpc/status"
18-
"net/http"
19-
"strconv"
20-
"time"
2122
)
2223

2324
func (controller *Controller) portForwardVM(ctx *gin.Context) responder.Responder {
24-
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
25+
if responder := controller.authorizeAny(ctx, v1.ServiceAccountRoleComputeWrite,
26+
v1.ServiceAccountRoleComputeConnect); responder != nil {
2527
return responder
2628
}
2729

internal/controller/sshserver/sshserver.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import (
55
"crypto/subtle"
66
"errors"
77
"fmt"
8+
"net"
9+
"strings"
10+
"time"
11+
812
"github.com/cirruslabs/orchard/internal/controller/notifier"
913
"github.com/cirruslabs/orchard/internal/controller/rendezvous"
1014
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
@@ -15,9 +19,6 @@ import (
1519
"github.com/samber/lo"
1620
"go.uber.org/zap"
1721
"golang.org/x/crypto/ssh"
18-
"net"
19-
"strings"
20-
"time"
2122
)
2223

2324
const (
@@ -110,9 +111,12 @@ func (server *SSHServer) passwordCallback(connMetadata ssh.ConnMetadata, passwor
110111
}
111112

112113
// Authorize
113-
if !lo.Contains(serviceAccount.Roles, v1.ServiceAccountRoleComputeWrite) {
114-
return fmt.Errorf("authorization failed for user %q because it lacks %q role",
115-
connMetadata.User(), v1.ServiceAccountRoleComputeWrite)
114+
authorized := lo.Contains(serviceAccount.Roles, v1.ServiceAccountRoleComputeWrite) ||
115+
lo.Contains(serviceAccount.Roles, v1.ServiceAccountRoleComputeConnect)
116+
117+
if !authorized {
118+
return fmt.Errorf("authorization failed for user %q because it lacks %q or %q roles",
119+
connMetadata.User(), v1.ServiceAccountRoleComputeWrite, v1.ServiceAccountRoleComputeConnect)
116120
}
117121

118122
return nil

pkg/resource/v1/service_account_role.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ var ErrUnsupportedServiceAccountRole = errors.New("unsupported service account r
1010
type ServiceAccountRole string
1111

1212
const (
13-
ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read"
14-
ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write"
15-
ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read"
16-
ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write"
13+
ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read"
14+
ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write"
15+
ServiceAccountRoleComputeConnect ServiceAccountRole = "compute:connect"
16+
ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read"
17+
ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write"
1718
)
1819

1920
func NewServiceAccountRole(name string) (ServiceAccountRole, error) {
@@ -22,6 +23,8 @@ func NewServiceAccountRole(name string) (ServiceAccountRole, error) {
2223
return ServiceAccountRoleComputeRead, nil
2324
case string(ServiceAccountRoleComputeWrite):
2425
return ServiceAccountRoleComputeWrite, nil
26+
case string(ServiceAccountRoleComputeConnect):
27+
return ServiceAccountRoleComputeConnect, nil
2528
case string(ServiceAccountRoleAdminRead):
2629
return ServiceAccountRoleAdminRead, nil
2730
case string(ServiceAccountRoleAdminWrite):
@@ -35,6 +38,7 @@ func AllServiceAccountRoles() []ServiceAccountRole {
3538
return []ServiceAccountRole{
3639
ServiceAccountRoleComputeRead,
3740
ServiceAccountRoleComputeWrite,
41+
ServiceAccountRoleComputeConnect,
3842
ServiceAccountRoleAdminRead,
3943
ServiceAccountRoleAdminWrite,
4044
}

0 commit comments

Comments
 (0)