-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix rawAcl
database scope
#1514
Changes from all commits
71fd2cb
d165f42
d39175b
223221e
5e269af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
from __future__ import annotations | ||
|
||
__version__ = "7.2.0" | ||
__version__ = "7.2.1" | ||
__api_subversion__ = "V20220125" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,9 +98,6 @@ def as_tuples(self) -> set[tuple]: | |
# Basic implementation for all simple Scopes (e.g. all or currentuser) | ||
return {(self._scope_name,)} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Surprised that these slipped through the reviews. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually kept it as a "simple 1:1 check", but decided now with the raw-database changes to not duplicate the error-prone scope logic. |
||
raise NotImplementedError | ||
|
||
@classmethod | ||
def from_tuple(cls, tpl: tuple) -> Self: | ||
acl_name, action, scope_name, *scope_params = tpl | ||
|
@@ -113,9 +110,9 @@ def from_tuple(cls, tpl: tuple) -> Self: | |
scope = scope_cls(scope_params) # type: ignore [call-arg] | ||
elif len(scope_params) == 2 and scope_cls is TableScope: | ||
db, tbl = scope_params | ||
scope = scope_cls({db: [tbl]}) # type: ignore [call-arg] | ||
scope = scope_cls({db: [tbl] if tbl else []}) # type: ignore [call-arg] | ||
else: | ||
raise ValueError(f"tuple not understood ({tpl})") | ||
raise ValueError(f"tuple not understood as capability: {tpl}") | ||
|
||
return cast(Self, capability_cls(actions=[capability_cls.Action(action)], scope=scope)) | ||
|
||
|
@@ -162,13 +159,6 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]: | |
capability_name = self._capability_name | ||
return {to_camel_case(capability_name) if camel_case else to_snake_case(capability_name): data} | ||
|
||
def has_capability(self, other: Capability) -> bool: | ||
if not isinstance(self, type(other)): | ||
return False | ||
if not other.scope.is_within(self.scope): | ||
return False | ||
return not set(other.actions) - set(self.actions) | ||
|
||
def as_tuples(self) -> set[tuple]: | ||
return set( | ||
(acl, action, *scope_tpl) | ||
|
@@ -262,17 +252,11 @@ def as_tuples(self, project: str | None = None) -> set[tuple]: | |
class AllScope(Capability.Scope): | ||
_scope_name = "all" | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, AllScope) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class CurrentUserScope(Capability.Scope): | ||
_scope_name = "currentuserscope" | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, (AllScope, CurrentUserScope)) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class IDScope(Capability.Scope): | ||
|
@@ -285,9 +269,6 @@ def __post_init__(self) -> None: | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, i) for i in self.ids} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, AllScope) or type(self) is type(other) and set(self.ids).issubset(other.ids) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class IDScopeLowerCase(Capability.Scope): | ||
|
@@ -302,9 +283,6 @@ def __post_init__(self) -> None: | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, i) for i in self.ids} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, AllScope) or type(self) is type(other) and set(self.ids).issubset(other.ids) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ExtractionPipelineScope(Capability.Scope): | ||
|
@@ -317,9 +295,6 @@ def __post_init__(self) -> None: | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, i) for i in self.ids} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, AllScope) or type(self) is type(other) and set(self.ids).issubset(other.ids) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class DataSetScope(Capability.Scope): | ||
|
@@ -332,9 +307,6 @@ def __post_init__(self) -> None: | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, i) for i in self.ids} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, AllScope) or type(self) is type(other) and set(self.ids).issubset(other.ids) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class TableScope(Capability.Scope): | ||
|
@@ -357,20 +329,9 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]: | |
return {self._scope_name: {key: {k: {"tables": v} for k, v in self.dbs_to_tables.items()}}} | ||
|
||
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, db, tbl) for db, tables in self.dbs_to_tables.items() for tbl in tables} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
if isinstance(other, AllScope): | ||
return True | ||
if not isinstance(other, TableScope): | ||
return False | ||
|
||
for db_name, tables in self.dbs_to_tables.items(): | ||
if (other_tables := other.dbs_to_tables.get(db_name)) is None: | ||
return False | ||
if not set(tables).issubset(other_tables): | ||
return False | ||
return True | ||
# When the scope contains no tables, it means all tables... since database name must be at least 1 | ||
# character, we represent this internally with the empty string: | ||
return {(self._scope_name, db, tbl) for db, tables in self.dbs_to_tables.items() for tbl in tables or [""]} | ||
|
||
|
||
@dataclass(frozen=True) | ||
|
@@ -384,9 +345,6 @@ def __post_init__(self) -> None: | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, i) for i in self.root_ids} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return isinstance(other, AllScope) or type(self) is type(other) and set(self.root_ids).issubset(other.root_ids) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ExperimentsScope(Capability.Scope): | ||
|
@@ -396,13 +354,6 @@ class ExperimentsScope(Capability.Scope): | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, s) for s in self.experiments} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return ( | ||
isinstance(other, AllScope) | ||
or type(self) is type(other) | ||
and set(self.experiments).issubset(other.experiments) | ||
) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class SpaceIDScope(Capability.Scope): | ||
|
@@ -412,11 +363,6 @@ class SpaceIDScope(Capability.Scope): | |
def as_tuples(self) -> set[tuple]: | ||
return {(self._scope_name, s) for s in self.space_ids} | ||
|
||
def is_within(self, other: Self) -> bool: | ||
return ( | ||
isinstance(other, AllScope) or type(self) is type(other) and set(self.space_ids).issubset(other.space_ids) | ||
) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class UnknownScope(Capability.Scope): | ||
|
@@ -434,9 +380,6 @@ def __getitem__(self, item: str) -> Any: | |
def as_tuples(self) -> set[tuple]: | ||
raise NotImplementedError("Unknown scope cannot be converted to tuples (needed for comparisons)") | ||
|
||
def is_within(self, other: Self) -> bool: | ||
raise NotImplementedError("Unknown scope cannot be compared") | ||
|
||
|
||
_SCOPE_CLASS_BY_NAME: MappingProxyType[str, type[Capability.Scope]] = MappingProxyType( | ||
{c._scope_name: c for c in Capability.Scope.__subclasses__() if not issubclass(c, UnknownScope)} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
ProjectCapabilityList, | ||
ProjectsAcl, | ||
RawAcl, | ||
TableScope, | ||
UnknownAcl, | ||
UnknownScope, | ||
) | ||
|
@@ -301,6 +302,8 @@ def test_project_scope_is_all_projects(self, proj_cap_allprojects_dct): | |
loaded = ProjectCapabilityList.load([proj_cap_allprojects_dct]) | ||
assert type(loaded[0].project_scope) is AllProjectsScope | ||
|
||
|
||
class TestIAMCompareCapabilities: | ||
@pytest.mark.parametrize( | ||
"capability", | ||
[ | ||
|
@@ -347,6 +350,40 @@ def test_partly_missing_capabilities( | |
) | ||
assert missing_acls == [has_not] | ||
|
||
def test_raw_acl_database_scope_only(self, cognite_client): | ||
# Would fail with: 'ValueError: No capabilities given' prior to 7.2.1 due to a bug in 'as_tuples'. | ||
has_all_scope = RawAcl([RawAcl.Action.Read], AllScope) | ||
has_db_scope = RawAcl([RawAcl.Action.Read], TableScope(dbs_to_tables={"db1": []})) | ||
assert not cognite_client.iam.compare_capabilities(has_all_scope, has_db_scope) | ||
|
||
@pytest.mark.parametrize( | ||
"extra_existing", | ||
[ | ||
[], | ||
[RawAcl(actions=[RawAcl.Action.Read], scope=AllScope)], | ||
[RawAcl(actions=[RawAcl.Action.Read], scope=TableScope({"db1": []}))], | ||
[RawAcl(actions=[RawAcl.Action.Read], scope=TableScope({"db1": {"tables": []}}))], | ||
Comment on lines
+364
to
+365
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are confusing, but if I understand it correctly, it means all in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means access is granted to all current and all future tables in the database I don't like the syntax one bit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, that's how the API does it, and hence we shouldn't add our own |
||
], | ||
) | ||
def test_raw_acl_database_scope(self, cognite_client, extra_existing): | ||
existing = [ | ||
RawAcl([RawAcl.Action.Read], RawAcl.Scope.Table({"db1": ["t1"]})), | ||
RawAcl([RawAcl.Action.Read], RawAcl.Scope.Table({"db1": ["t1", "t2"]})), | ||
RawAcl([RawAcl.Action.Read], RawAcl.Scope.Table({"db2": ["t1", "t2"]})), | ||
*extra_existing, | ||
] | ||
desired = [ | ||
RawAcl([RawAcl.Action.Read], RawAcl.Scope.Table({"db1": ["t1", "t2", "t3"]})), | ||
RawAcl([RawAcl.Action.Write], RawAcl.Scope.Table({"db1": ["t1"]})), | ||
] | ||
missing = cognite_client.iam.compare_capabilities(existing, desired) | ||
if extra_existing: | ||
assert missing == [RawAcl([RawAcl.Action.Write], RawAcl.Scope.Table({"db1": ["t1"]}))] | ||
else: | ||
assert len(missing) == 2 | ||
assert RawAcl([RawAcl.Action.Read], RawAcl.Scope.Table({"db1": ["t3"]})) in missing | ||
assert RawAcl([RawAcl.Action.Write], RawAcl.Scope.Table({"db1": ["t1"]})) in missing | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"dct", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would you want this set to True?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is the most logical to me. You have access to all data sets when you have all, which would be the most common use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want two groups of capabilities to be equal, and they differ in scopes "not all" - but you still have some all-scopes here and there, then this option will tell you exactly what those differences are. For Peter's bootstrap CLI (and maybe others...) it means the manual work of removing any possible all-scopes or raw-database-scopes before comparing is not needed.