Skip to content

Commit

Permalink
Merge pull request #6318 from weaviate/rbac_ref_tests
Browse files Browse the repository at this point in the history
[RBAC] Add no-rights role and fix references
  • Loading branch information
dirkkul authored Nov 13, 2024
2 parents 0005fbc 933ceef commit ed3db4b
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 26 deletions.
6 changes: 3 additions & 3 deletions docker-compose-auth-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 28,7 @@ services:
DISABLE_RECOVERY_ON_PANIC: "true"
AUTHORIZATION_ENABLE_RBAC: "true"
AUTHENTICATION_APIKEY_ENABLED: "true"
AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'viewer-key,editor-key,admin-key'
AUTHENTICATION_APIKEY_USERS: 'viewer-user,editor-user,admin-user'
AUTHENTICATION_APIKEY_ROLES: 'viewer,editor,admin'
AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'viewer-key,editor-key,admin-key,no-rights-key'
AUTHENTICATION_APIKEY_USERS: 'viewer-user,editor-user,admin-user,no-rights-user'
AUTHENTICATION_APIKEY_ROLES: 'viewer,editor,admin,no-rights'

Empty file.
10 changes: 10 additions & 0 deletions test/acceptance_with_python/rbac/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 1,10 @@
def _sanitize_role_name(name: str) -> str:
return (
name.replace("[", "")
.replace("]", "")
.replace("-", "")
.replace(" ", "")
.replace(".", "")
.replace("{", "")
.replace("}", "")
)
110 changes: 110 additions & 0 deletions test/acceptance_with_python/rbac/test_rbac_refs.py
Original file line number Diff line number Diff line change
@@ -0,0 1,110 @@
from typing import Union, List

import pytest
import weaviate
import weaviate.classes as wvc
from weaviate.rbac.models import RBAC, DatabaseAction, CollectionsAction
from _pytest.fixtures import SubRequest
from .conftest import _sanitize_role_name


def test_rbac_users(request: SubRequest):
with weaviate.connect_to_local(
port=8081, grpc_port=50052, auth_credentials=wvc.init.Auth.api_key("admin-key")
) as client:
client.collections.delete(["target", "source"])
# create two collections with some objects to test refs
target = client.collections.create(name="target")
source = client.collections.create(
name="source",
references=[wvc.config.ReferenceProperty(name="ref", target_collection=target.name)],
)
uuid_target1 = target.data.insert({})
uuid_target2 = target.data.insert({})
uuid_source = source.data.insert(properties={}, references={"ref": uuid_target1})
role_name = _sanitize_role_name(request.node.name)
client.roles.delete(role_name)

# read update for both
with weaviate.connect_to_local(
port=8081, grpc_port=50052, auth_credentials=wvc.init.Auth.api_key("no-rights-key")
) as client_no_rights:
both_write = client.roles.create(
name=role_name,
permissions=RBAC.permissions.collection(
target.name, CollectionsAction.UPDATE_COLLECTIONS
)
RBAC.permissions.collection(target.name, CollectionsAction.READ_COLLECTIONS)
RBAC.permissions.collection(source.name, CollectionsAction.UPDATE_COLLECTIONS)
RBAC.permissions.collection(source.name, CollectionsAction.READ_COLLECTIONS),
)
client.roles.assign(user="no-rights-user", roles=both_write.name)

source_no_rights = client_no_rights.collections.get(
source.name
) # no network call => no RBAC check
source_no_rights.data.reference_add(
from_uuid=uuid_source,
from_property="ref",
to=uuid_target1,
)

source_no_rights.data.reference_replace(
from_uuid=uuid_source,
from_property="ref",
to=uuid_target2,
)

source_no_rights.data.reference_delete(
from_uuid=uuid_source,
from_property="ref",
to=uuid_target2,
)

client.roles.revoke(user="no-rights-user", roles=both_write.name)
client.roles.delete(both_write.name)

# only read update for one of them
for col in [source.name]:
with weaviate.connect_to_local(
port=8081, grpc_port=50052, auth_credentials=wvc.init.Auth.api_key("no-rights-key")
) as client_no_rights:
role = client.roles.create(
name=role_name,
permissions=RBAC.permissions.collection(
col, CollectionsAction.UPDATE_COLLECTIONS
)
RBAC.permissions.collection(col, CollectionsAction.READ_COLLECTIONS),
)
client.roles.assign(user="no-rights-user", roles=role.name)

source_no_rights = client_no_rights.collections.get(
source.name
) # no network call => no RBAC check

with pytest.raises(weaviate.exceptions.UnexpectedStatusCodeException) as e:
source_no_rights.data.reference_add(
from_uuid=uuid_source,
from_property="ref",
to=uuid_target1,
)
assert e.value.status_code == 403

with pytest.raises(weaviate.exceptions.UnexpectedStatusCodeException) as e:
source_no_rights.data.reference_replace(
from_uuid=uuid_source,
from_property="ref",
to=uuid_target2,
)
assert e.value.status_code == 403

with pytest.raises(weaviate.exceptions.UnexpectedStatusCodeException) as e:
source_no_rights.data.reference_delete(
from_uuid=uuid_source,
from_property="ref",
to=uuid_target1,
)
assert e.value.status_code == 403

client.roles.revoke(user="no-rights-user", roles=role.name)
client.roles.delete(role.name)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 5,7 @@
import weaviate.classes as wvc
from weaviate.rbac.models import RBAC, DatabaseAction, CollectionsAction
from _pytest.fixtures import SubRequest
from .conftest import _sanitize_role_name

db = RBAC.actions.database
col = RBAC.actions.collection
Expand All @@ -13,18 14,19 @@
@pytest.mark.parametrize(
"database_permissions",
[
db.READ_COLLECTIONS,
db.READ_ROLES,
db.MANAGE_ROLES,
[db.READ_COLLECTIONS, db.CREATE_COLLECTIONS],
[db.MANAGE_CLUSTER, db.READ_ROLES],
],
)
@pytest.mark.parametrize(
"collection_permissions",
[
col.READ_COLLECTIONS,
col.CREATE_OBJECTS,
col.CREATE_TENANTS,
[col.CREATE_TENANTS, col.UPDATE_OBJECTS, col.DELETE_OBJECTS],
[col.READ_COLLECTIONS, col.CREATE_COLLECTIONS],
],
)
def test_rbac_roles_admin(
Expand Down Expand Up @@ -56,7 58,7 @@ def test_rbac_users(request: SubRequest):
with weaviate.connect_to_local(
port=8081, grpc_port=50052, auth_credentials=wvc.init.Auth.api_key("admin-key")
) as client:
database_permissions = RBAC.actions.database.CREATE_COLLECTIONS
database_permissions = RBAC.actions.database.READ_ROLES
num_roles = 2
role_names = [_sanitize_role_name(request.node.name) str(i) for i in range(num_roles)]
for role_name in role_names:
Expand All @@ -73,15 75,3 @@ def test_rbac_users(request: SubRequest):

for role_name in role_names:
client.roles.delete(role_name)


def _sanitize_role_name(name: str) -> str:
return (
name.replace("[", "")
.replace("]", "")
.replace("-", "")
.replace(" ", "")
.replace(".", "")
.replace("{", "")
.replace("}", "")
)
2 changes: 1 addition & 1 deletion test/acceptance_with_python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 1,4 @@
git https://github.com/weaviate/weaviate-python-client.git@002ea0028a2eaa62e779436a57ee27fb6e585935
git https://github.com/weaviate/weaviate-python-client.git@4994f7748b1bbcaa07a449b2b16f0f80ab3a11cc

pytest>=8.0.1,<9.0.0
pytest-xdist==3.6.1
Expand Down
3 changes: 3 additions & 0 deletions usecases/auth/authorization/rbac/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 108,9 @@ func Init(authConfig config.APIKey, policyPath string) (*casbin.SyncedCachedEnfo

// add pre existing roles
for name, verb := range builtInPolicies {
if verb == "" {
continue
}
if _, err := enforcer.AddNamedPolicy("p", name, "*", verb, "*"); err != nil {
return nil, fmt.Errorf("add policy: %w", err)
}
Expand Down
14 changes: 8 additions & 6 deletions usecases/auth/authorization/rbac/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 86,17 @@ var (
)

var builtInPolicies = map[string]string{
"viewer": authorization.READ,
"editor": authorization.CRU,
"admin": authorization.CRUD,
"no-rights": "",
"viewer": authorization.READ,
"editor": authorization.CRU,
"admin": authorization.CRUD,
}

var builtInPermissions = map[string][]*models.Permission{
"viewer": {readAllCollections},
"editor": {createAllCollections, readAllCollections, updateAllCollections},
"admin": {manageAllRoles, manageAllCluster, createAllCollections, readAllCollections, updateAllCollections, deleteAllCollections},
"no-rights": {},
"viewer": {readAllCollections},
"editor": {createAllCollections, readAllCollections, updateAllCollections},
"admin": {manageAllRoles, manageAllCluster, createAllCollections, readAllCollections, updateAllCollections, deleteAllCollections},
}

type Policy struct {
Expand Down
12 changes: 11 additions & 1 deletion usecases/objects/references_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 16,8 @@ import (
"errors"
"fmt"

autherrs "github.com/weaviate/weaviate/usecases/auth/authorization/errors"

"github.com/weaviate/weaviate/usecases/auth/authorization"

"github.com/go-openapi/strfmt"
Expand Down Expand Up @@ -69,6 71,11 @@ func (m *Manager) AddObjectReference(ctx context.Context, principal *models.Prin
if errors.As(err, &ErrMultiTenancy{}) {
return &Error{"validate inputs", StatusUnprocessableEntity, err}
}
var forbidden autherrs.Forbidden
if errors.As(err, &forbidden) {
return &Error{"validate inputs", StatusForbidden, err}
}

return &Error{"validate inputs", StatusBadRequest, err}
}

Expand All @@ -83,6 90,9 @@ func (m *Manager) AddObjectReference(ctx context.Context, principal *models.Prin
targetRef.Class = string(toClass)
}
}
if err := m.authorizer.Authorize(principal, authorization.READ, authorization.Shards(targetRef.Class, tenant)...); err != nil {
return &Error{err.Error(), StatusForbidden, err}
}

if err := input.validateExistence(ctx, validator, tenant, targetRef); err != nil {
return &Error{"validate existence", StatusBadRequest, err}
Expand Down Expand Up @@ -172,8 182,8 @@ func (req *AddReferenceInput) validate(
if err != nil {
return nil, nil, 0, err
}

vclass := vclasses[req.Class]

return ref, vclass.Class, vclass.Version, validateReferenceSchema(sm, vclass.Class, req.Property)
}

Expand Down
4 changes: 4 additions & 0 deletions usecases/objects/references_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 64,9 @@ func (m *Manager) DeleteObjectReference(ctx context.Context, principal *models.P
input.Reference.Beacon = toBeacon
}
}
if err := m.authorizer.Authorize(principal, authorization.READ, authorization.Shards(input.Reference.Class.String(), tenant)...); err != nil {
return &Error{err.Error(), StatusForbidden, err}
}

res, err := m.getObjectFromRepo(ctx, input.Class, input.ID,
additional.Properties{}, nil, tenant)
Expand All @@ -74,6 77,7 @@ func (m *Manager) DeleteObjectReference(ctx context.Context, principal *models.P
} else if errors.As(err, &ErrMultiTenancy{}) {
return &Error{"source object", StatusUnprocessableEntity, err}
}

return &Error{"source object", StatusInternalServerError, err}
}
input.Class = res.ClassName
Expand Down
4 changes: 4 additions & 0 deletions usecases/objects/references_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 97,10 @@ func (m *Manager) UpdateObjectReferences(ctx context.Context, principal *models.
parsedTargetRefs[i].Class = string(toClass)
}
}
if err := m.authorizer.Authorize(principal, authorization.READ, authorization.Shards(input.Refs[i].Class.String(), tenant)...); err != nil {
return &Error{err.Error(), StatusForbidden, err}
}

if err := input.validateExistence(ctx, validator, tenant, parsedTargetRefs[i]); err != nil {
return &Error{"validate existence", StatusBadRequest, err}
}
Expand Down

0 comments on commit ed3db4b

Please sign in to comment.