-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
feat(auth) - New privilege for Associate entities for entities tags and glossary terms #8644
base: master
Are you sure you want to change the base?
Changes from 15 commits
c923b8b
f60a413
0da4478
5067773
0ee58b0
e03e2af
67a991f
df12d4a
e5216d4
7f002d5
c35550a
2750e54
15d56b1
80efa94
c60d9ab
bf81623
193a093
74074fb
f37a807
fae9dd0
4c1aa72
f703c95
8b43d10
49977c1
b235f0d
e98fb1e
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 |
---|---|---|
|
@@ -39,6 +39,9 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw | |
return GraphQLConcurrencyUtils.supplyAsync( | ||
() -> { | ||
|
||
// First, validate the tag urns | ||
validateTags(tagUrns, context); | ||
|
||
// First, validate the batch | ||
validateInputResources(context.getOperationContext(), resources, context); | ||
|
||
|
@@ -57,6 +60,14 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw | |
"get"); | ||
} | ||
|
||
private void validateTags(List<Urn> tagUrns, QueryContext context) { | ||
for (Urn tagUrn : tagUrns) { | ||
if (!LabelUtils.isAuthorizedToAssociateTag(context, tagUrn)) { | ||
throw new AuthorizationException("Only users granted permission to this tag can assign or remove it"); | ||
} | ||
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. now in all these resolvers we'd be checking they have the 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. EDIT_ENTITY_TAGS_PRIVILEGE is a privilege for the entity to which a list of tags are associated. ASSOCIATE_TAGS_PRIVILEGE is for the list of tags that are being associated. That is the reason I have created 2 different methods. |
||
} | ||
} | ||
|
||
private void validateInputResources( | ||
@Nonnull OperationContext opContext, List<ResourceRefInput> resources, QueryContext context) { | ||
for (ResourceRefInput resource : resources) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -258,6 +258,21 @@ public static boolean isAuthorizedToUpdateTags( | |
orPrivilegeGroups); | ||
} | ||
|
||
public static boolean isAuthorizedToAssociateTag(@Nonnull QueryContext context, Urn targetUrn) { | ||
|
||
// Decide whether the current principal should be allowed to associate the tag | ||
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( | ||
new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.ASSOCIATE_TAGS_PRIVILEGE.getType())) | ||
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. hm I'm a little confused here - so what's the benefit of this new privilege when we have the 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.
Please check my response to your other comment on the need for this new privilege |
||
)); | ||
|
||
return AuthorizationUtils.isAuthorized( | ||
context.getAuthorizer(), | ||
context.getActorUrn(), | ||
targetUrn.getEntityType(), | ||
targetUrn.toString(), | ||
orPrivilegeGroups); | ||
} | ||
|
||
public static boolean isAuthorizedToUpdateTerms( | ||
@Nonnull QueryContext context, Urn targetUrn, String subResource) { | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,6 +97,12 @@ function getGraphqlErrorCode(e) { | |
|
||
export const handleBatchError = (urns, e, defaultMessage) => { | ||
if (urns.length > 1 && getGraphqlErrorCode(e) === 403) { | ||
if (e.message === 'Only users granted permission to this tag can assign or remove it') { | ||
return { | ||
content: `${e.message}. The bulk edit being performed will not be saved.`, | ||
duration: 3, | ||
}; | ||
} | ||
Comment on lines
+100
to
+105
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. why do we need this new message when we have a message about being unauthorized below? 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. The new error message is for clarity to pinpoint which authorization is missing. |
||
return { | ||
content: | ||
'Your bulk edit selection included entities that you are unauthorized to update. The bulk edit being performed will not be saved.', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||
import { Link } from 'react-router-dom'; | ||
import { Form, Select, Tag, Tooltip, Typography, Tag as CustomTag } from 'antd'; | ||
import { Form, Select, Tag, Tooltip, Typography, Tag as CustomTag, Checkbox } from 'antd'; | ||
import styled from 'styled-components/macro'; | ||
|
||
import { useEntityRegistry } from '../../useEntityRegistry'; | ||
|
@@ -9,13 +9,14 @@ import { | |
useGetSearchResultsForMultipleLazyQuery, | ||
useGetSearchResultsLazyQuery, | ||
} from '../../../graphql/search.generated'; | ||
import { ResourceFilter, PolicyType, EntityType, Domain, Entity } from '../../../types.generated'; | ||
import { ResourceFilter, PolicyType, EntityType, Domain, Entity, PolicyMatchCondition } from '../../../types.generated'; | ||
import { | ||
convertLegacyResourceFilter, | ||
createCriterionValue, | ||
createCriterionValueWithEntity, | ||
EMPTY_POLICY, | ||
getFieldValues, | ||
getFieldCondition, | ||
getFieldValuesOfTags, | ||
mapResourceTypeToDisplayName, | ||
mapResourceTypeToEntityType, | ||
|
@@ -105,6 +106,8 @@ export default function PolicyPrivilegeForm({ | |
const resourceTypes = getFieldValues(resources.filter, 'TYPE') || []; | ||
const resourceEntities = getFieldValues(resources.filter, 'URN') || []; | ||
|
||
const matchConditionInitial = getFieldCondition(resources.filter, 'RESOURCE_URN'); | ||
const [matchCondition, setMatchCondition] = useState(matchConditionInitial); | ||
const getDisplayName = (entity) => { | ||
if (!entity) { | ||
return null; | ||
|
@@ -180,7 +183,7 @@ export default function PolicyPrivilegeForm({ | |
}; | ||
setResources({ | ||
...resources, | ||
filter: setFieldValues(filter, 'TYPE', [...resourceTypes, createCriterionValue(selectedResourceType)]), | ||
filter: setFieldValues(filter, 'TYPE', [...resourceTypes, createCriterionValue(selectedResourceType)], PolicyMatchCondition.Equals), | ||
}); | ||
}; | ||
|
||
|
@@ -194,6 +197,7 @@ export default function PolicyPrivilegeForm({ | |
filter, | ||
'TYPE', | ||
resourceTypes?.filter((criterionValue) => criterionValue.value !== deselectedResourceType), | ||
PolicyMatchCondition.Equals, | ||
), | ||
}); | ||
}; | ||
|
@@ -211,7 +215,9 @@ export default function PolicyPrivilegeForm({ | |
resource, | ||
getEntityFromSearchResults(resourceSearchResults, resource) || null, | ||
), | ||
]), | ||
], | ||
matchCondition, | ||
), | ||
}); | ||
}; | ||
|
||
|
@@ -226,20 +232,40 @@ export default function PolicyPrivilegeForm({ | |
filter, | ||
'URN', | ||
resourceEntities?.filter((criterionValue) => criterionValue.value !== resource), | ||
matchCondition, | ||
), | ||
}); | ||
}; | ||
|
||
const updateMatchConditionInResources = (excludeResource) => { | ||
const filter = resources.filter || { | ||
criteria: [], | ||
}; | ||
setResources({ | ||
...resources, | ||
filter: setFieldValues( | ||
filter, | ||
'RESOURCE_URN', | ||
resourceEntities, | ||
excludeResource ? PolicyMatchCondition.NotEquals : PolicyMatchCondition.Equals, | ||
), | ||
}); | ||
}; | ||
// When a domain is selected, add its urn to the list of domains | ||
const onSelectDomain = (domainUrn, domainObj?: Domain) => { | ||
const filter = resources.filter || { | ||
criteria: [], | ||
}; | ||
const domainEntity = domainObj || getEntityFromSearchResults(domainSearchResults, domainUrn); | ||
const updatedFilter = setFieldValues(filter, 'DOMAIN', [ | ||
const updatedFilter = setFieldValues( | ||
filter, | ||
'DOMAIN', | ||
[ | ||
...domains, | ||
createCriterionValueWithEntity(domainUrn, domainEntity || null), | ||
]); | ||
], | ||
PolicyMatchCondition.Equals, | ||
); | ||
setResources({ | ||
...resources, | ||
filter: updatedFilter, | ||
|
@@ -262,6 +288,7 @@ export default function PolicyPrivilegeForm({ | |
filter, | ||
'DOMAIN', | ||
domains?.filter((criterionValue) => criterionValue.value !== domain), | ||
PolicyMatchCondition.Equals, | ||
), | ||
}); | ||
}; | ||
|
@@ -321,6 +348,23 @@ export default function PolicyPrivilegeForm({ | |
: displayStr; | ||
}; | ||
|
||
const getResourceText = (policyMatch) => { | ||
if (policyMatch === PolicyMatchCondition.Equals) { | ||
return ( | ||
<Typography.Paragraph> | ||
Search for specific resources the policy should apply to. If <b>none</b> is selected, policy is | ||
applied to <b>all</b> resources of the given type. | ||
</Typography.Paragraph> | ||
); | ||
} | ||
return ( | ||
<Typography.Paragraph> | ||
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 is introducing a non-trivial amount of new risk into this system by adding a lot of new complexity. Is there consideration about priority of exclusion inclusion rules and conflicting rules? |
||
Search for specific resource(s) the policy exclusion should apply to. If <b>none</b> is selected, policy | ||
is applied to <b>all</b> resources of the given type. | ||
</Typography.Paragraph> | ||
); | ||
}; | ||
|
||
function handleCLickOutside() { | ||
// delay closing the domain navigator so we don't get a UI "flash" between showing search results and navigator | ||
setTimeout(() => setIsFocusedOnInput(false), 0); | ||
|
@@ -488,39 +532,57 @@ export default function PolicyPrivilegeForm({ | |
</Form.Item> | ||
)} | ||
{showResourceFilterInput && ( | ||
<Form.Item label={<Typography.Text strong>Resource</Typography.Text>}> | ||
<Typography.Paragraph> | ||
Search for specific resources the policy should apply to. If <b>none</b> is selected, policy is | ||
applied to <b>all</b> resources of the given type. | ||
</Typography.Paragraph> | ||
<Select | ||
notFoundContent="No search results found" | ||
value={resourceSelectValue} | ||
mode="multiple" | ||
filterOption={false} | ||
placeholder="Apply to ALL resources by default. Select specific resources to apply to." | ||
onSelect={onSelectResource} | ||
onDeselect={onDeselectResource} | ||
onSearch={handleResourceSearch} | ||
tagRender={(tagProps) => ( | ||
<Tag closable={tagProps.closable} onClose={tagProps.onClose}> | ||
<Tooltip title={tagProps.value.toString()}> | ||
{displayStringWithMaxLength( | ||
resourceUrnToDisplayName[tagProps.value.toString()] || | ||
tagProps.value.toString(), | ||
75, | ||
)} | ||
</Tooltip> | ||
</Tag> | ||
)} | ||
> | ||
{resourceSearchResults?.map((result) => ( | ||
<Select.Option key={result.entity.urn} value={result.entity.urn}> | ||
{renderSearchResult(result)} | ||
</Select.Option> | ||
))} | ||
</Select> | ||
</Form.Item> | ||
<> | ||
<Form.Item label={<Typography.Text strong>Resource Condition</Typography.Text>}> | ||
<Typography.Paragraph> | ||
Selecting the checkbox below will exclude selected resource from the policy. If not | ||
selected, resources selected will be included in the policy. | ||
</Typography.Paragraph> | ||
<Checkbox | ||
checked={matchCondition === PolicyMatchCondition.NotEquals} | ||
onChange={(value) => { | ||
setMatchCondition( | ||
value?.target?.checked | ||
? PolicyMatchCondition.NotEquals | ||
: PolicyMatchCondition.Equals, | ||
); | ||
updateMatchConditionInResources(value?.target?.checked); | ||
}} | ||
> | ||
Exclude Resources | ||
</Checkbox> | ||
</Form.Item> | ||
<Form.Item label={<Typography.Text strong>Resource</Typography.Text>}> | ||
{getResourceText(matchCondition)} | ||
<Select | ||
notFoundContent="No search results found" | ||
value={resourceSelectValue} | ||
mode="multiple" | ||
filterOption={false} | ||
placeholder="Apply to ALL resources by default. Select specific resources to apply to." | ||
onSelect={onSelectResource} | ||
onDeselect={onDeselectResource} | ||
onSearch={handleResourceSearch} | ||
tagRender={(tagProps) => ( | ||
<Tag closable={tagProps.closable} onClose={tagProps.onClose}> | ||
<Tooltip title={tagProps.value.toString()}> | ||
{displayStringWithMaxLength( | ||
resourceUrnToDisplayName[tagProps.value.toString()] || | ||
tagProps.value.toString(), | ||
75, | ||
)} | ||
</Tooltip> | ||
</Tag> | ||
)} | ||
> | ||
{resourceSearchResults?.map((result) => ( | ||
<Select.Option key={result.entity.urn} value={result.entity.urn}> | ||
{renderSearchResult(result)} | ||
</Select.Option> | ||
))} | ||
</Select> | ||
</Form.Item> | ||
</> | ||
)} | ||
{showResourceFilterInput && ( | ||
<Form.Item label={<Typography.Text strong>Select Tags</Typography.Text>}> | ||
|
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.
Auth calls should come first before sending validation operations to the entity service, ideally done as soon as possible before any business logic occurs to short circuit.