-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New feature UMA authorization for syncStations
- Loading branch information
Showing
13 changed files
with
376 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# SPDX-FileCopyrightText: NOI Techpark <digital@noi.bz.it> | ||
# SPDX-License-Identifier: CC0-1.0 | ||
# | ||
##### VSCODE / REST Client | ||
# Create a .env file and set the corresponding variables | ||
# See all $dotenv fields below | ||
|
||
@host = https://auth.opendatahub.testingmachine.eu/auth/realms/noi | ||
|
||
### Get access token for UMA tests | ||
# @name login | ||
POST {{host}}/protocol/openid-connect/token | ||
Content-Type: application/x-www-form-urlencoded | ||
|
||
&grant_type=client_credentials | ||
&client_id=odh-mobility-dev-uma | ||
&client_secret=STFLg69cUgZS8nNgtUQIuZDMiPQBIipe | ||
&scope=openid | ||
|
||
### | ||
@authtoken = {{login.response.body.access_token}} | ||
|
||
# See here for documentation of the API: https://www.keycloak.org/docs/23.0.7/authorization_services/#_service_overview | ||
|
||
### Get endpoints and capabilities | ||
GET {{host}}/.well-known/uma2-configuration | ||
|
||
### Get permissions | ||
GET {{host}}/authz/protection/resource_set | ||
Authorization: Bearer {{authtoken}} | ||
|
||
&grant_type=urn:ietf:params:oauth:grant-type:uma-ticket |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
writer/src/main/java/com/opendatahub/timeseries/bdp/writer/writer/authz/Authorization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// SPDX-FileCopyrightText: NOI Techpark <digital@noi.bz.it> | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-or-later | ||
package com.opendatahub.timeseries.bdp.writer.writer.authz; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
import org.keycloak.authorization.client.AuthorizationDeniedException; | ||
import org.keycloak.authorization.client.AuthzClient; | ||
import org.keycloak.authorization.client.resource.AuthorizationResource; | ||
import org.keycloak.representations.idm.authorization.AuthorizationRequest; | ||
import org.keycloak.representations.idm.authorization.Permission; | ||
import org.keycloak.representations.idm.authorization.ResourceRepresentation; | ||
import org.springframework.cache.annotation.Cacheable; | ||
|
||
public class Authorization { | ||
public static final String ATTRIBUTE_AUTHORIZATION = "bdp_authz"; | ||
|
||
private AuthzClient authz; | ||
private AuthorizationResource authzRes; | ||
private String clientId; | ||
|
||
public Authorization(AuthzClient client, String accessToken) { | ||
this.authz = client; | ||
this.authzRes = client.authorization(accessToken); // Authorization resource initialized with the requestor's credentials | ||
this.clientId = authz.getConfiguration().getResource(); | ||
} | ||
|
||
public boolean hasAnyAuthorization() { | ||
try { | ||
authzRes.authorize(); | ||
return true; | ||
} catch (AuthorizationDeniedException e) { | ||
return false; | ||
} | ||
} | ||
|
||
public List<ResourceRepresentation> getAuthorizedResources(String type, String scope) { | ||
// https://github.com/keycloak/keycloak/issues/27483 | ||
var req = new AuthorizationRequest(); | ||
req.setMetadata(new AuthorizationRequest.Metadata()); | ||
req.setAudience(clientId); | ||
req.setScope(scope); | ||
|
||
var perms = authzRes.getPermissions(req); | ||
|
||
Set<String> resourceIds; | ||
// https://github.com/keycloak/keycloak/issues/16520 | ||
// runtime type is wrong until fix is merged | ||
if (!perms.isEmpty() && ((List) perms).get(0) instanceof Map) { | ||
// Forcefully cast this to map (which it actually is at runtime) | ||
List<Object> tmp = new ArrayList<>(perms); | ||
resourceIds = tmp.stream() | ||
.map(o -> (Map<String, String>) o) | ||
.map(m -> m.get("rsid")) | ||
.collect(Collectors.toSet()); | ||
} else { | ||
resourceIds = perms.stream().map(Permission::getResourceId).collect(Collectors.toSet()); | ||
} | ||
|
||
return getAllResources().stream() | ||
.filter(r -> resourceIds.contains(r.getId())) | ||
.toList(); | ||
} | ||
|
||
@Cacheable("resources") | ||
private List<ResourceRepresentation> getAllResources() { | ||
// get all resource details from Keycloak | ||
// TODO: caching | ||
var protect = authz.protection(); | ||
return protect.resource().<List<ResourceRepresentation>>find(null, null, null, null, null, null, false, true, | ||
null, null); | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
...rc/main/java/com/opendatahub/timeseries/bdp/writer/writer/authz/AuthorizeSyncStation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// SPDX-FileCopyrightText: NOI Techpark <digital@noi.bz.it> | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-or-later | ||
// | ||
package com.opendatahub.timeseries.bdp.writer.writer.authz; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.function.BooleanSupplier; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.web.util.UriComponents; | ||
import org.springframework.web.util.UriComponentsBuilder; | ||
|
||
import com.opendatahub.timeseries.bdp.dto.dto.StationDto; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
|
||
public class AuthorizeSyncStation { | ||
private static final Logger log = LoggerFactory.getLogger(AuthorizeSyncStation.class); | ||
|
||
public static void authorize(HttpServletRequest req, String stationType, List<StationDto> dtos, boolean syncState, | ||
boolean onlyActivation) { | ||
var authz = (Authorization) req.getAttribute(Authorization.ATTRIBUTE_AUTHORIZATION); | ||
if (authz == null) { | ||
log.warn("No UMA authorization configuration found. Maybe your client credentials are incomplete or incorrect?"); | ||
throw new NotAuthorizedException("Not authorized"); | ||
} | ||
log.debug("Start authorizing station sync"); | ||
|
||
var origins = dtos.stream() | ||
.map(StationDto::getOrigin) | ||
.collect(Collectors.toSet()); | ||
|
||
// Technically we could handle multiple origins, but the state synchronization | ||
// doesn't behave correctly anyway | ||
if (origins.size() != 1) { | ||
throw new NotAuthorizedException("mixed or missing origins"); | ||
} | ||
var origin = origins.stream().findFirst().get(); | ||
|
||
log.debug("Start evaluating UMA for stationType = {}, origin = {}, syncState = {}, onlyActivation = {}", stationType, origin, syncState, onlyActivation); | ||
|
||
var authorizedResources = authz.getAuthorizedResources("station", "write"); | ||
log.debug("Got authorized resources from server: {}", authorizedResources); | ||
|
||
boolean authorized = authorizedResources.stream() | ||
.flatMap(r -> r.getUris().stream()) | ||
.filter(u -> uriMatches(u, stationType, origin, syncState, onlyActivation)) | ||
.findAny().isPresent(); | ||
|
||
log.debug("Authorization on resource granted: {}", authorized); | ||
|
||
if (!authorized){ | ||
throw new NotAuthorizedException("Missing authorization"); | ||
} | ||
} | ||
|
||
private record Test(String name, BooleanSupplier condition){} | ||
|
||
public static boolean uriMatches(String uri, String stationType, String origin, boolean syncState, boolean onlyActivation) { | ||
var u = UriComponentsBuilder.fromUriString(uri).build(); | ||
|
||
log.debug("Checking URI {}", u); | ||
return Arrays.stream(new Test[] { | ||
new Test("scheme", () -> "bdp".equals(u.getScheme())), | ||
new Test("authority", () -> "station".equals(u.getHost())), | ||
new Test("stationType", () -> getQueryParam(u, "stationType").anyMatch(s -> s.equals(stationType))), | ||
new Test("origin", () -> getQueryParam(u, "origin").anyMatch(s -> s.equals(origin))), | ||
new Test("syncState", () -> getQueryParam(u, "syncState").map(Boolean::parseBoolean).anyMatch(b -> b == syncState)), | ||
new Test("onlyActivation", () -> getQueryParam(u, "onlyActivation").map(Boolean::parseBoolean).anyMatch(b -> b == onlyActivation)) | ||
}) | ||
.allMatch(t -> { | ||
boolean result = t.condition.getAsBoolean(); | ||
log.debug("Check {}: {}", t.name, result); | ||
return result; | ||
}); | ||
} | ||
|
||
private static Stream<String> getQueryParam(UriComponents u, String param){ | ||
return u.getQueryParams().getOrDefault(param, Collections.emptyList()).stream(); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
.../main/java/com/opendatahub/timeseries/bdp/writer/writer/authz/NotAuthorizedException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// SPDX-FileCopyrightText: NOI Techpark <digital@noi.bz.it> | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-or-later | ||
package com.opendatahub.timeseries.bdp.writer.writer.authz; | ||
|
||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.server.ResponseStatusException; | ||
|
||
public class NotAuthorizedException extends ResponseStatusException { | ||
public NotAuthorizedException(String reason) { | ||
super(HttpStatus.UNAUTHORIZED, reason); | ||
} | ||
} |
Oops, something went wrong.