Skip to content

Commit

Permalink
New permission ClientsRead and new router middleware RequireAtLeastOn…
Browse files Browse the repository at this point in the history
…ePermission (#324)

* feat: add new permission ClientsRead, assign ClientsRead to role User, revoke ClientsManage from role User

* feat: introduce new middleware PermissionsCheckAtLeastOne to check that a requester has at least one listed permission in order to access a given resource

* chore: ensure comment accuracy
  • Loading branch information
elikmiller authored Jan 19, 2024
1 parent e51afb3 commit bc9064a
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 28 deletions.
30 changes: 23 additions & 7 deletions cmd/api/src/api/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package middleware
Expand Down Expand Up @@ -99,14 +99,30 @@ func AuthMiddleware(authenticator api.Authenticator) mux.MiddlewareFunc {
}
}

// PermissionsCheck is a middleware func generator that returns a http.Handler which closes around a list of
// PermissionsCheckAll is a middleware func generator that returns a http.Handler which closes around a list of
// permissions that an actor must have in the request auth context to access the wrapped http.Handler.
func PermissionsCheck(authorizer auth.Authorizer, permissions ...model.Permission) mux.MiddlewareFunc {
func PermissionsCheckAll(authorizer auth.Authorizer, permissions ...model.Permission) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if bhCtx := ctx.FromRequest(request); !bhCtx.AuthCtx.Authenticated() {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusUnauthorized, "not authenticated", request), response)
} else if !authorizer.AllowsAllPermissions(bhCtx.AuthCtx, permissions) {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, "not authorized", request), response)
} else {
next.ServeHTTP(response, request)
}
})
}
}

// PermissionsCheckAtLeastOne is a middleware func generator that returns a http.Handler which closes around a list of
// permissions that an actor must have at least one in the request auth context to access the wrapped http.Handler.
func PermissionsCheckAtLeastOne(authorizer auth.Authorizer, permissions ...model.Permission) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if bhCtx := ctx.FromRequest(request); !bhCtx.AuthCtx.Authenticated() {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusUnauthorized, "not authenticated", request), response)
} else if !authorizer.AllowsPermissions(bhCtx.AuthCtx, permissions) {
} else if !authorizer.AllowsAtLeastOnePermission(bhCtx.AuthCtx, permissions) {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, "not authorized", request), response)
} else {
next.ServeHTTP(response, request)
Expand Down
140 changes: 129 additions & 11 deletions cmd/api/src/api/middleware/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package middleware
Expand All @@ -21,18 +21,22 @@ import (
"testing"
"time"

"github.com/specterops/bloodhound/headers"
"github.com/specterops/bloodhound/src/api"
"github.com/specterops/bloodhound/src/auth"
"github.com/specterops/bloodhound/src/ctx"
"github.com/specterops/bloodhound/src/model"
"github.com/specterops/bloodhound/src/test/must"
"github.com/specterops/bloodhound/src/utils/test"
"github.com/stretchr/testify/require"
"github.com/specterops/bloodhound/headers"
)

func permissionsCheckHandler(internalHandler http.HandlerFunc, permissions ...model.Permission) http.Handler {
return PermissionsCheck(auth.NewAuthorizer(), permissions...)(internalHandler)
func permissionsCheckAllHandler(internalHandler http.HandlerFunc, permissions ...model.Permission) http.Handler {
return PermissionsCheckAll(auth.NewAuthorizer(), permissions...)(internalHandler)
}

func permissionsCheckAtLeastOneHandler(internalHandler http.HandlerFunc, permissions ...model.Permission) http.Handler {
return PermissionsCheckAtLeastOne(auth.NewAuthorizer(), permissions...)(internalHandler)
}

func Test_parseAuthorizationHeader(t *testing.T) {
Expand All @@ -52,7 +56,7 @@ func Test_parseAuthorizationHeader(t *testing.T) {
require.Nil(t, err)
}

func TestPermissionsCheck(t *testing.T) {
func TestPermissionsCheckAll(t *testing.T) {
var (
handlerReturn200 = func(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(http.StatusOK)
Expand All @@ -63,7 +67,7 @@ func TestPermissionsCheck(t *testing.T) {
WithURL("http//example.com").
WithHeader(headers.RequestID.String(), "requestID").
WithContext(&ctx.Context{}).
OnHandler(permissionsCheckHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
OnHandler(permissionsCheckAllHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
Require().
ResponseStatusCode(http.StatusUnauthorized)

Expand All @@ -83,7 +87,121 @@ func TestPermissionsCheck(t *testing.T) {
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
OnHandler(permissionsCheckAllHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
Require().
ResponseStatusCode(http.StatusForbidden)

test.Request(t).
WithURL("http//example.com").
WithHeader(headers.RequestID.String(), "requestID").
WithContext(&ctx.Context{
AuthCtx: auth.Context{
PermissionOverrides: auth.PermissionOverrides{},
Owner: model.User{
Roles: model.Roles{
{
Name: "Big Boy",
Description: "The big boy.",
Permissions: auth.Permissions().All(),
},
},
},
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckAllHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
Require().
ResponseStatusCode(http.StatusOK)
}

func TestPermissionsCheckAtLeastOne(t *testing.T) {
var (
handlerReturn200 = func(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(http.StatusOK)
}
)

test.Request(t).
WithURL("http//example.com").
WithContext(&ctx.Context{
AuthCtx: auth.Context{
PermissionOverrides: auth.PermissionOverrides{},
Owner: model.User{
Roles: model.Roles{
{
Name: "Big Boy",
Description: "The big boy.",
Permissions: model.Permissions{auth.Permissions().AuthManageSelf},
},
},
},
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckAtLeastOneHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
Require().
ResponseStatusCode(http.StatusOK)

test.Request(t).
WithURL("http//example.com").
WithContext(&ctx.Context{
AuthCtx: auth.Context{
PermissionOverrides: auth.PermissionOverrides{},
Owner: model.User{
Roles: model.Roles{
{
Name: "Big Boy",
Description: "The big boy.",
Permissions: model.Permissions{auth.Permissions().AuthManageSelf, auth.Permissions().GraphDBRead},
},
},
},
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckAtLeastOneHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
Require().
ResponseStatusCode(http.StatusOK)

test.Request(t).
WithURL("http//example.com").
WithContext(&ctx.Context{
AuthCtx: auth.Context{
PermissionOverrides: auth.PermissionOverrides{},
Owner: model.User{
Roles: model.Roles{
{
Name: "Big Boy",
Description: "The big boy.",
Permissions: model.Permissions{auth.Permissions().AuthManageSelf, auth.Permissions().GraphDBRead},
},
},
},
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckAtLeastOneHandler(handlerReturn200, auth.Permissions().GraphDBRead)).
Require().
ResponseStatusCode(http.StatusOK)

test.Request(t).
WithURL("http//example.com").
WithContext(&ctx.Context{
AuthCtx: auth.Context{
PermissionOverrides: auth.PermissionOverrides{},
Owner: model.User{
Roles: model.Roles{
{
Name: "Big Boy",
Description: "The big boy.",
Permissions: model.Permissions{auth.Permissions().AuthManageSelf, auth.Permissions().GraphDBRead},
},
},
},
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckAtLeastOneHandler(handlerReturn200, auth.Permissions().GraphDBWrite)).
Require().
ResponseStatusCode(http.StatusForbidden)

Expand All @@ -105,7 +223,7 @@ func TestPermissionsCheck(t *testing.T) {
Session: model.UserSession{},
},
}).
OnHandler(permissionsCheckHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
OnHandler(permissionsCheckAtLeastOneHandler(handlerReturn200, auth.Permissions().AuthManageSelf)).
Require().
ResponseStatusCode(http.StatusOK)
}
9 changes: 8 additions & 1 deletion cmd/api/src/api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,15 @@ func (s *Route) RequireAuth() *Route {
return s.RequirePermissions()
}

// Ensure that the requestor has all of the listed permissions
func (s *Route) RequirePermissions(permissions ...model.Permission) *Route {
s.handler.Use(middleware.PermissionsCheck(s.authorizer, permissions...))
s.handler.Use(middleware.PermissionsCheckAll(s.authorizer, permissions...))
return s
}

// Ensure that the requestor has at least one of the listed permissions
func (s *Route) RequireAtLeastOnePermission(permissions ...model.Permission) *Route {
s.handler.Use(middleware.PermissionsCheckAtLeastOne(s.authorizer, permissions...))
return s
}

Expand Down
27 changes: 19 additions & 8 deletions cmd/api/src/auth/model.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package auth
Expand All @@ -23,11 +23,11 @@ import (
"strconv"
"time"

"github.com/specterops/bloodhound/src/database/types/null"
"github.com/specterops/bloodhound/src/model"
"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt/v4"
"github.com/specterops/bloodhound/errors"
"github.com/specterops/bloodhound/src/database/types/null"
"github.com/specterops/bloodhound/src/model"
)

const (
Expand Down Expand Up @@ -85,7 +85,8 @@ func (s idResolver) GetIdentity(ctx Context) (SimpleIdentity, error) {

type Authorizer interface {
AllowsPermission(ctx Context, requiredPermission model.Permission) bool
AllowsPermissions(ctx Context, requiredPermissions model.Permissions) bool
AllowsAllPermissions(ctx Context, requiredPermissions model.Permissions) bool
AllowsAtLeastOnePermission(ctx Context, requiredPermissions model.Permissions) bool
}

type authorizer struct{}
Expand All @@ -106,7 +107,7 @@ func (s authorizer) AllowsPermission(ctx Context, requiredPermission model.Permi
return false
}

func (s authorizer) AllowsPermissions(ctx Context, requiredPermissions model.Permissions) bool {
func (s authorizer) AllowsAllPermissions(ctx Context, requiredPermissions model.Permissions) bool {
for _, permission := range requiredPermissions {
if !s.AllowsPermission(ctx, permission) {
return false
Expand All @@ -116,6 +117,16 @@ func (s authorizer) AllowsPermissions(ctx Context, requiredPermissions model.Per
return true
}

func (s authorizer) AllowsAtLeastOnePermission(ctx Context, requiredPermissions model.Permissions) bool {
for _, permission := range requiredPermissions {
if s.AllowsPermission(ctx, permission) {
return true
}
}

return false
}

type Context struct {
PermissionOverrides PermissionOverrides
Owner any
Expand Down
5 changes: 5 additions & 0 deletions cmd/api/src/auth/permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type PermissionSet struct {

SavedQueriesRead model.Permission
SavedQueriesWrite model.Permission

ClientsRead model.Permission
}

func (s PermissionSet) All() model.Permissions {
Expand All @@ -64,6 +66,7 @@ func (s PermissionSet) All() model.Permissions {
s.APsManageAPs,
s.SavedQueriesRead,
s.SavedQueriesWrite,
s.ClientsRead,
}
}

Expand Down Expand Up @@ -92,5 +95,7 @@ func Permissions() PermissionSet {

SavedQueriesRead: model.NewPermission("saved_queries", "Read"),
SavedQueriesWrite: model.NewPermission("saved_queries", "Write"),

ClientsRead: model.NewPermission("clients", "Read"),
}
}
2 changes: 1 addition & 1 deletion cmd/api/src/auth/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ func Roles() map[string]RoleTemplate {
Description: "Can read data, modify asset group memberships",
Permissions: model.Permissions{
permissions.GraphDBRead,
permissions.ClientsManage,
permissions.AuthCreateToken,
permissions.AuthManageSelf,
permissions.APsGenerateReport,
permissions.AppReadApplicationConfiguration,
permissions.SavedQueriesRead,
permissions.SavedQueriesWrite,
permissions.ClientsRead,
},
},
RoleAdministrator: {
Expand Down

0 comments on commit bc9064a

Please sign in to comment.