Skip to content
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

Authorization on operations #3662

Merged
merged 4 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .changesets/feat_geal_authorization_directives.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### GraphOS Enterprise: authorization directives ([PR #3397](https://github.com/apollographql/router/pull/3397))
### GraphOS Enterprise: authorization directives ([PR #3397](https://github.com/apollographql/router/pull/3397), [PR #3662](https://github.com/apollographql/router/pull/3662))

We introduce two new directives, `@authenticated` and `requiresScopes`, that define authorization policies for field and types in the supergraph schema.

Expand All @@ -19,4 +19,4 @@ They are implemented by hooking the request lifecycle at multiple steps:
- at the execution service level, the response is formatted according to the filtered query first, which will remove any unauthorized information, then to the shape of the original query, which will propagate nulls as needed
- at the execution service level, errors are added to the response indicating which fields were removed because they were not authorized

By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3397
By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3397 https://github.com/apollographql/router/pull/3662
23 changes: 20 additions & 3 deletions apollo-router/src/plugins/authorization/authenticated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,24 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> {
self.compiler
}

fn operation(
&mut self,
node: &hir::OperationDefinition,
) -> Result<Option<apollo_encoder::OperationDefinition>, BoxError> {
let operation_requires_authentication = node
.object_type(&self.compiler.db)
.map(|ty| ty.directive_by_name(AUTHENTICATED_DIRECTIVE_NAME).is_some())
.unwrap_or(false);

if operation_requires_authentication {
self.unauthorized_paths.push(self.current_path.clone());
self.query_requires_authentication = true;
Ok(None)
} else {
transform::operation(self, node)
}
}

fn field(
&mut self,
parent_type: &str,
Expand Down Expand Up @@ -322,6 +340,7 @@ mod tests {

type Mutation @authenticated {
ping: User
other: String
}

interface I {
Expand Down Expand Up @@ -395,9 +414,7 @@ mod tests {
fn mutation() {
static QUERY: &str = r#"
mutation {
ping {
name
}
other
}
"#;

Expand Down
46 changes: 45 additions & 1 deletion apollo-router/src/plugins/authorization/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ impl<'a> traverse::Visitor for PolicyExtractionVisitor<'a> {
self.compiler
}

fn operation(&mut self, node: &hir::OperationDefinition) -> Result<(), BoxError> {
if let Some(ty) = node.object_type(&self.compiler.db) {
self.extracted_policies
.extend(policy_argument(ty.directive_by_name(POLICY_DIRECTIVE_NAME)).cloned());
}

traverse::operation(self, node)
}

fn field(&mut self, parent_type: &str, node: &hir::Field) -> Result<(), BoxError> {
if let Some(ty) = self
.compiler
Expand Down Expand Up @@ -323,6 +332,39 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> {
self.compiler
}

fn operation(
&mut self,
node: &hir::OperationDefinition,
) -> Result<Option<apollo_encoder::OperationDefinition>, BoxError> {
let is_authorized = if let Some(ty) = node.object_type(&self.compiler.db) {
match ty.directive_by_name(POLICY_DIRECTIVE_NAME) {
None => true,
Some(directive) => {
let type_policies = policy_argument(Some(directive))
.cloned()
.collect::<HashSet<_>>();
// The field is authorized if any of the policies succeeds
type_policies.is_empty()
|| self
.request_policies
.intersection(&type_policies)
.next()
.is_some()
}
}
} else {
false
};

if is_authorized {
transform::operation(self, node)
} else {
self.unauthorized_paths.push(self.current_path.clone());
self.query_requires_policies = true;
Ok(None)
}
}

fn field(
&mut self,
parent_type: &str,
Expand Down Expand Up @@ -490,8 +532,9 @@ mod tests {
itf: I
}

type Mutation {
type Mutation @policy(policies: ["mut"]) {
ping: User @policy(policies: ["ping"])
other: String
}

interface I {
Expand Down Expand Up @@ -645,6 +688,7 @@ mod tests {
ping {
name
}
other
}
"#;

Expand Down
49 changes: 48 additions & 1 deletion apollo-router/src/plugins/authorization/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> {
self.compiler
}

fn operation(&mut self, node: &hir::OperationDefinition) -> Result<(), BoxError> {
if let Some(ty) = node.object_type(&self.compiler.db) {
self.extracted_scopes.extend(
scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned(),
);
}

traverse::operation(self, node)
}

fn field(&mut self, parent_type: &str, node: &hir::Field) -> Result<(), BoxError> {
if let Some(ty) = self
.compiler
Expand Down Expand Up @@ -374,6 +384,41 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> {
self.compiler
}

fn operation(
&mut self,
node: &hir::OperationDefinition,
) -> Result<Option<apollo_encoder::OperationDefinition>, BoxError> {
let is_authorized = if let Some(ty) = node.object_type(&self.compiler.db) {
match ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME) {
None => true,
Some(directive) => {
let mut type_scopes_sets = scopes_sets_argument(directive);

// The outer array acts like a logical OR: if any of the inner arrays of scopes matches, the field
// is authorized.
// On an empty set, any returns false, so we must check that case separately
let mut empty = true;
let res = type_scopes_sets.any(|scopes_set| {
empty = false;
self.request_scopes.is_superset(&scopes_set)
});

empty || res
}
}
} else {
false
};

if is_authorized {
transform::operation(self, node)
} else {
self.unauthorized_paths.push(self.current_path.clone());
self.query_requires_scopes = true;
Ok(None)
}
}

fn field(
&mut self,
parent_type: &str,
Expand Down Expand Up @@ -548,8 +593,9 @@ mod tests {
itf: I
}

type Mutation {
type Mutation @requiresScopes(scopes: [["mut"]]) {
ping: User @requiresScopes(scopes: [["ping"]])
other: String
}

interface I {
Expand Down Expand Up @@ -770,6 +816,7 @@ mod tests {
ping {
name
}
other
}
"#;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ expression: "TestResult { query: QUERY, result: doc, paths }"
query:

mutation {
ping {
name
}
other
}

filtered:

paths: ["/ping"]
paths: [""]
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ expression: paths
---
[
Path(
[
Key(
"ping",
),
],
[],
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source: apollo-router/src/plugins/authorization/policy.rs
expression: doc
---
{
"mut",
"ping",
"read user",
"read username",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ query:
ping {
name
}
other
}

extracted_scopes: {"ping", "read:user", "read:username"}
extracted_scopes: {"mut", "ping", "read:user", "read:username"}
request scopes: []
filtered:

paths: ["/ping"]
paths: [""]