-
Notifications
You must be signed in to change notification settings - Fork 8k
[admin-v2] Polymorphism, refined OIDC Client representation #44727
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
Conversation
f2a27e2 to
437966d
Compare
|
@vmuzikar Thanks for the PR! Spotless is failing. |
|
@vmuzikar Could you please provide some PR description of provided changes + caveats + what's necessary to complete to mark it as ready to review? Thanks |
mabartos
left a comment
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.
Just a brief look.
rest/admin-v2/api/src/main/java/org/keycloak/models/mapper/AbstractRepModelMapper.java
Outdated
Show resolved
Hide resolved
| Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions); | ||
|
|
||
| Stream<ClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions); | ||
| Stream<BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions); | ||
|
|
||
| Stream<ClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions); | ||
| Stream<BaseClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions); |
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.
| Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions); | |
| Stream<ClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions); | |
| Stream<BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions); | |
| Stream<ClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions); | |
| Stream<BaseClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions); | |
| Optional<? extends BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions); | |
| Stream<? extends BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions); | |
| Stream<? extends BaseClientRepresentation> deleteClients(RealmModel realm, ClientSearchOptions searchOptions); |
Wouldn't it be better to use the wildcard of the type?
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.
We'd need to do that on the REST layer too, I believe. And there we don't want it, IMHO.
437966d to
d0d6d65
Compare
|
I started on the operator controller logic again based upon this pr - a couple of additional thoughts:
So you would still have a oneOf stucture for the supported protocols, but you'd only need a single controller for all client types. The only big reason not to do this is if we think that the client subclass somehow changes the behavior of base client fields (even removing them). Also the CRD generator would not understand this directly, but we of course don't have to fully rely on that for generation in the first iteration and we'll eventually have fabric8io/kubernetes-client#7220 to make whatever manipulations we need. |
7a11367 to
4f9354c
Compare
|
Added some notes to the desc, fixed spotless. This should be now ready for full review. Thanks. |
I think that's fine if we document it properly and add some clear error handling.
Does it mean we'd have a single Client CR? |
Yes it does. The other way to do this without a structural schema, and which is supported by the CRD Generator, is to add each possible protocol as sibling fields. Like ValueOrSecret, or EnvFromSource, etc. The controller then validates that only 1 is populated. EDIT: it is possible to have a single CR even with using the full subclassing. You can have the top-level of the CR spec as the structure part of the schema (or the representations as siblings). It's just not as typical of an approach. |
4f9354c to
7717db7
Compare
|
@shawkins IMHO separate CRs are a better approach than a single one. It's better UX for the users that they see only fields relevant to given client type. If the number of types gets out of our hands, we can always introduce a different approach l ike configmaps instead of CRs. |
After thinking more about the controller implementation, I'd prefer a single CRD. I could confirm in the openshift console how it presents structural schema in the editor if you are open to considering it. |
Ok, let's explore that. |
After looking more into the structural schema capabilities, I'd say they are still limited. The validation contructs anyOf, oneOf, etc. can only references properties, and those properties must be already defined in the schema. There is also no support the concept of a descriminator. The above prevents us from doing something that looks like this: Instead you still have to use sibling properties: Even saml would probably still be typed as object to allow for future field specialization - but initially it would be ackward specifying it as an empty object. The openshift editor of course won't prevent you from entering both siblings. If you try to save it with both populated, you'll get a message like: So either I'm missing something obvious or the facilties for doing this still aren't that great. |
|
@shawkins Thanks for looking into this further. Sounds like the separate CRs is the way forward then? |
Yes, we'll have to create base controller logic and run a controller per type. |
shawkins
left a comment
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.
Looks good. Just a couple of thoughts / nits.
I'm also not sure we need a model mapper provider yet as it's internal, not configurable, and has no alternative implementations - but it's not a deal breaker to leave in for now.
| PathItem sortedPathItem = OASFactory.createPathItem(); | ||
|
|
||
| // Add operations order: GET -> POST -> PUT -> PATCH -> DELETE -> HEAD -> OPTIONS -> TRACE | ||
| if (pathItem.getGET() != null) { |
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.
Just a nit: null checks don't seem to be required.
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 was introduced in some previous PR, I just moved things around. Seemed safer to check for nulls just in case there are some defaults for whatever reason in the newly created path item.
| static void validateUnknownFields(ClientRepresentation rep) { | ||
| if (!rep.getAdditionalFields().isEmpty()) { | ||
| static void validateUnknownFields(BaseClientRepresentation rep) { | ||
| if (rep.getAdditionalFields().keySet().stream().anyMatch(k -> !k.equals(BaseClientRepresentation.DISCRIMINATOR_FIELD))) { |
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 having the protocol field not be part of the schema is causing it to be parsed into the additionalFields, perhaps we should just have it as a proper field.
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'm more inclined to keep as is, not to have arbitrary discriminator fields directly in the representation. We can (and most probably will) revisit as a follow-up.
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 someone uses these classes on the client side, they'll have to do something similar if they want to check for unknown fields.
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.
Ok, created #45047 so we can address it as a follow-up if you don't mind.
|
@shawkins Thank you for the review!
The motivation was that I expected the client types to be extendible, so I wanted to implement it in the most generic way possible. |
7717db7 to
ade9bd8
Compare
Closes keycloak#43290 Signed-off-by: Václav Muzikář <[email protected]>
Signed-off-by: Václav Muzikář <[email protected]>
ade9bd8 to
13754b1
Compare
…#44727) * [admin-v2] Polymorphism, refined OIDC Client representation Closes keycloak#43290 Signed-off-by: Václav Muzikář <[email protected]> * Remove AbstractRepModelMapper Signed-off-by: Václav Muzikář <[email protected]> --------- Signed-off-by: Václav Muzikář <[email protected]> Signed-off-by: sar <[email protected]>
Closes #43290
Couple of notes:
OIDCClientModelMapperwith generic methods liketoModel(BaseClientRepresentation rep)that would in fact map specificOIDCClientRepresentation.@Mapping.oneOf.Follow-ups: