-
Notifications
You must be signed in to change notification settings - Fork 142
Add annotation based user search #2565
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
base: feature/ONYX-17902
Are you sure you want to change the base?
Conversation
| annotation = Annotation.fetch_annotation(id: id.split(':')[-1].underscore.to_sym) | ||
| if annotation | ||
| id = annotation.resource_id | ||
| end |
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.
It is not at all obvious why annotations are involved here. Mind adding more comments through these code changes explaining what they're for?
|
|
||
| Role[role_id: id] || raise(ApplicationController::Forbidden) | ||
| end | ||
| end No newline at end of file |
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.
There is a VS Code setting that will make sure the final newline is present automatically. I would recommend configuring that so you don't have to think about these.
files.insertFinalNewline
More info here: https://stackoverflow.com/a/44704969
app/models/annotation.rb
Outdated
| query = "value = '#{id}'" | ||
| return query unless account && service_id | ||
|
|
||
| "#{query} and name = 'authn-oidc/#{service_id}' and resource_id LIKE '%#{account}%'" |
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.
Seeing 'authn-oidc` in this file is a big red flag to me. The annotation model shouldn't know anything about OIDC. Particularly because the reference is in a string, this introduces a hidden dependency and coupling that is hard to discover and will almost certainly cause future pain.
I'm still trying to digest the changeset as a whole, but on this point in particular, I would expect to see something more like an OIDCAnnotation class or similar that wraps Annotation to provide the OIDC domain-specific methods.
This probably means that there need to be variants of a generic Role and an OIDCRole. This would cascade further to the find_current_user method to use the authenticator plugin interface to allow each plugin to attempt to resolve the current user, rather than hardcode a special case for OIDC.
Let me know what makes sense and what I should elaborate on further. In general, special cases are a code smell, and we should instead design our code to follow single, generic workflows with pluggable interfaces to supply domain specific logic that implement the generic flow in a specific way (e.g. finding the current user by OIDC username).
6e3f27d to
e332389
Compare
| annotation = Annotation.fetch_annotation( | ||
| account: authenticator.account, | ||
| value: identity, | ||
| name: authenticator.authenticator_name | ||
| ).first | ||
| return @role_repository_class.from_username(authenticator.account, identity) unless annotation | ||
|
|
||
| @role_repository_class[role_id: annotation.resource_id] |
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.
How come the default/base implementation for fetching a role is using annotations? I would expect this to be solely based on the ID, and I'm not sure why this change is necessary
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.
I agree with the order of selection concern. Let's use the following for the resolution order:
- Find user by ID - essentially, this requires users to be in the root
- Find user by annotation (
authn-oidc/identity) - allows users to exist outside the root policy, but runs the risk of two users with the same OIDC identity
Defaulting to User ID as the default lookup maintains a similar behavior as other authenticators, but still allows for annotation based lookup.
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.
@andytinkham would love your thoughts on the find user by annotation scenario where multiple users have same annotation and how to handle this from a security best practice.
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.
I was also wondering if a user could have multiple annotations - for example email and username and are we searching both??
| annotation = Annotation.fetch_annotation(account: account, value: id).first | ||
| return id unless annotation |
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.
This definitely needs a good comment describing what's going on here.
This is still giving precedence to annotations over role ID's, correct?
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.
from what i understood that is the intention. i asked while talking about this feature with jason if we wanted to default to role_id, then use annotation if it didn't match and Jason said it was to be the other way around. This wasn't about a specific section of code tho but more of a high level discussion
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.
Hey @jvanderhoof , can you give any additional context to why we would give precedence for annotation values over resource IDs when resolving OIDC identities?
The risk I'm considering is that this allows a host to set an annotation and impersonate an existing role with the host ID used for the mapping. Also, it's generally been an implied design principle that annotations are a weaker security control than IDs or variables. So taking an annotation as the higher precedent over the ID doesn't seem to jive with that either.
I'm probably missing some background or context that's important here, though.
CC: @andytinkham
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.
@micahlee, as mentioned above, with annotations, I'm hoping to provide a path for getting users with email addresses (common OIDC pattern) mapped to Conjur users (where we use the "unfortunate" @ to delineate policy scope for a user - my.user@my-policy).
Annotations have emerged as a common pattern for attributing external ID to internal ID. I agree it's less secure. We should absolutely keep pushing on this to understand how best to manage to "Users in root" requirement and tie internal/external mapping.
app/models/annotation.rb
Outdated
| end | ||
| end | ||
|
|
||
| def self.fetch_annotation(account: nil, value:, name: nil) |
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.
@mFelgate, what's the scenario where an annotation doesn't have a value? This feels problematic for looking up a resources as it may find an annotation related to a resource which is unrelated to OIDC (or the target annotation).
| end | ||
|
|
||
| def authenticator_name | ||
| return self.service_id |
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.
In Ruby the last line is returned, so the return is redundant here.
Alternatively, we can just add an alias:
module Authenticator
class Authenticator
alias authenticator_name, service_id
...
end
end|
|
||
| def fetch_conjur_role(authenticator, identity) | ||
| return @role_repository_class.from_username(authenticator.account, identity) | ||
| annotation = Annotation.fetch_annotation( |
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.
Instead of including the Annotation dependency here, I'd recommend injecting it into the Authentication::Handler::AuthenticationHandler initialization method.
This will allow you to write tests for this handler without loading anything into the database.
| @@ -130,7 +130,14 @@ def validate_client_ip(client_ip_address, conjur_role) | |||
| end | |||
|
|
|||
| def fetch_conjur_role(authenticator, identity) | |||
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.
I think we should consider moving this into either a Role or a User repository. This allows us to decouple the process of finding the relevant user from the authentication process (which this class does).
We could do something like this (warning pseudocode) :
module DB
module Repository
class RoleRepository
def initialize(role: ::Role, annotation: ::Annotation)
@role = role
@annotation = annotation
end
def find(account:, identifier:, annotation: nil, type: 'user')
role = find_by_id(account: account, identifier: identifier, type: type)
if role.blank? && annotation.present?
role = find_by_annotation(account: account, identifier: identifier, type: type, annotation: annotation)
end
role
end
# The methods below are public to simplify testing, but only `find` should be part of the primary interface.
def find_by_id(account:, identifier:, type: 'user')
@role[[account, type, identifier].join(':')]
end
def find_by_annotation(account:, identifier:, annotation:, type: 'user')
resource_id = @annotation
.where(name: annotation) # exact match on annotation name
.ilike(value: identifier) # case insensitive match on annotation value
.like(resource_id: "#{account}:#{type}:%") # verify account and type
.first
.try(:resource_id)
return nil unless resource_id
@role[resource_id]
end
end
end
endAlternatively, we can extend Annotation to include a method to handle finding a resource given an annotation name and value. In this case, the Annotation method should return the resource_id to align with the Role#roleid_from_username convention.
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.
@jvanderhoof, do you want a role repo or would this be fine to just add to the role model?
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.
The problem is Annotation. Adding find_by_annotation to Role feels a bit weird as it forces Annotation as a dependency on Role.
It might make more sense to add a find_by_key_and_value type method to Annotation.
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.
in that case should it be more like the original, but just have role_id be the default
| - !user | ||
| id: alice | ||
| annotations: | ||
| authn-oidc/keycloak: alice.somebody@cyberark.com |
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.
I was thinking about the annotation more along the lines of:
- !user
id: alice
annotations:
authn-oidc/identity: alice.somebody@cyberark.comwhich would allow the user to be selected given a JWT with the subject alice.somebody@cyberark.com. This lines up with the way annotations are used for mapping in other authenticators: Azure (checkout the "Define Azure Authenticator policy" policy sample).
11006b6 to
0ce2084
Compare
e1e1dc7 to
f028d38
Compare
6ebb0ec to
dcaa5b4
Compare
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.
I have a few questions:
- can this annotation be anything (ie: not just email) so long as it matches user defined in JWT token
- what happens if duplicate annotations for different users - if these are not unique this could be security concern
- can hosts have this annotation?
d210521 to
9536b8e
Compare
| @@ -0,0 +1,74 @@ | |||
| # frozen_string_literal: 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.
Nice work on these specs, @mFelgate!
I read back through https://www.betterspecs.org/ and https://martinfowler.com/articles/mocksArentStubs.html and I think what you've done is a perfectly reasonably approach:
- You've mocked out the RoleRepository dependencies
- Those get passed to a real
RoleRepositoryobject - None of the internals of
RoleRepositoryare mocked
| ) | ||
| end | ||
|
|
||
| describe('.find') do |
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.
The only nit from reading better spec again is that this should follow the Ruby conventions for referring to a method:
| describe('.find') do | |
| describe('#find') do |
dcaa5b4 to
7f0f7f1
Compare
bceeb65 to
16f9d72
Compare
7f0f7f1 to
d57f23d
Compare
d57f23d to
17aaf89
Compare
6369cf1 to
97eb2f9
Compare
Desired Outcome
Please describe the desired outcome for this PR. Said another way, what was
the original request that resulted in these code changes? Feel free to copy
this information from the connected issue.
Implemented Changes
Describe how the desired outcome above has been achieved with this PR. In
particular, consider:
Connected Issue/Story
Resolves #[relevant GitHub issue(s), e.g. 76]
CyberArk internal issue link: insert issue ID
Definition of Done
At least 1 todo must be completed in the sections below for the PR to be
merged.
Changelog
CHANGELOG update
Test coverage
changes, or
Documentation
READMEs) were updated in this PRBehavior
Security