From 9408fc9055db0ec646fcfb68c20d68df37d67f4a Mon Sep 17 00:00:00 2001 From: Matthias Cullmann Date: Thu, 25 Apr 2024 11:45:33 +0200 Subject: [PATCH] shared secrets see https://learn.microsoft.com/en-us/azure/azure-functions/security-concepts and #1 --- .../baloise/azure/FunctionalOrgEndpoint.java | 46 ++++++++++++------- src/main/java/com/baloise/azure/Graph.java | 16 +++++-- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/baloise/azure/FunctionalOrgEndpoint.java b/src/main/java/com/baloise/azure/FunctionalOrgEndpoint.java index 7988292..232a4bf 100644 --- a/src/main/java/com/baloise/azure/FunctionalOrgEndpoint.java +++ b/src/main/java/com/baloise/azure/FunctionalOrgEndpoint.java @@ -7,11 +7,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import com.azure.identity.ClientSecretCredential; @@ -34,15 +36,12 @@ * Azure Functions with HTTP Trigger. */ public class FunctionalOrgEndpoint { - /** - * This function listens at endpoint "/api/Hello". Two ways to invoke it using - * "curl" command in bash: 1. curl -d "HTTP Body" {your host}/api/Hello 2. curl - * "{your host}/api/Hello?name=HTTP%20Query" - */ Vault lazyVault = null; Graph lazygraph = null; ObjectMapper objectMapper = new ObjectMapper(); + private Set lazyValidTokens; + final static String TOKEN_KEY = "TOKENS"; Vault vault() { if(lazyVault == null) { @@ -51,7 +50,7 @@ Vault vault() { return lazyVault; } - Graph graph() { + Graph graph(boolean obfuscated) { if(lazygraph == null) { final String[] scopes = new String[] { AzureProperties.defaultScope() }; final ClientSecretCredential credential = @@ -64,9 +63,9 @@ Graph graph() { .build(); - lazygraph = new Graph(credential, scopes); + lazygraph = new Graph(credential, scopes, obfuscated); } - return lazygraph; + return lazygraph.withObfuscation(obfuscated); } @FunctionName("V1") @@ -83,17 +82,18 @@ public HttpResponseMessage v1( try { List path = asList(request.getUri().getPath().split("/")); path = path.subList(path.indexOf("V1")+1, path.size()); + boolean obfuscated = !hasValidToken(request); if(path.size() ==1 && "~roleSchemes".equals(path.get(0))) { - return createJSONResponse(request, Map.of("default", graph().getDefaultRoleScheme(), "roleSchemes", graph().getRoleSchemes())); + return createJSONResponse(request, Map.of("default", graph(obfuscated).getDefaultRoleScheme(), "roleSchemes", graph(obfuscated).getRoleSchemes())); } else if(path.size() ==2 && "~avatar".equals(path.get(0))) { - return createAvatarResponse(request, path.get(1)); + return createAvatarResponse(request, path.get(1), obfuscated); } - if(request.getQueryParameters().containsKey("clear")) graph().clear(); - final StringTree child = graph().getOrg().getChild(path.toArray(new String[0])); + if(request.getQueryParameters().containsKey("clear")) graph(obfuscated).clear(); + final StringTree child = graph(obfuscated).getOrg().getChild(path.toArray(new String[0])); - return child.isLeaf() ? createTeamResponse(request, context, child) : createOrganisationResponse(request, context, child); + return child.isLeaf() ? createTeamResponse(request, context, child, obfuscated) : createOrganisationResponse(request, context, child); } catch (Throwable t) { context.getLogger().log(Level.WARNING, t.getLocalizedMessage(), t); @@ -101,8 +101,20 @@ public HttpResponseMessage v1( } } - private HttpResponseMessage createAvatarResponse(HttpRequestMessage> request, String id) throws IOException { - byte[] avatar = graph().avatar(id); + private boolean hasValidToken(HttpRequestMessage> request) { + String token = request.getQueryParameters().get("token"); + return token != null ? validTokens().contains(token) : false; + } + + private Set validTokens() { + if(lazyValidTokens == null) { + lazyValidTokens = Collections.singleton(vault().getSecret(TOKEN_KEY, true)); + } + return lazyValidTokens; + } + + private HttpResponseMessage createAvatarResponse(HttpRequestMessage> request, String id, boolean obfuscated) throws IOException { + byte[] avatar = graph(obfuscated).avatar(id); String myETag = String.valueOf(Arrays.hashCode(avatar)); String theirETag = ignoreKeyCase(request.getHeaders()).get("If-None-Match"); boolean sameEtag = Objects.equals(myETag, theirETag); @@ -130,10 +142,10 @@ public String get(Object key) { return lowMap; } - private HttpResponseMessage createTeamResponse(HttpRequestMessage> request, ExecutionContext context, StringTree team) + private HttpResponseMessage createTeamResponse(HttpRequestMessage> request, ExecutionContext context, StringTree team, boolean obfuscated) throws JsonProcessingException { - final Map body = graph().loadTeam(team.getProperty("id"), getRoles(request)); + final Map body = graph(obfuscated).loadTeam(team.getProperty("id"), getRoles(request)); body.put("name", team.getName()); body.put("url", format("%s/%s", getPath(request),team.getName())); return createJSONResponse(request, body); diff --git a/src/main/java/com/baloise/azure/Graph.java b/src/main/java/com/baloise/azure/Graph.java index d7a6011..33d28fa 100644 --- a/src/main/java/com/baloise/azure/Graph.java +++ b/src/main/java/com/baloise/azure/Graph.java @@ -21,7 +21,6 @@ import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import com.azure.identity.ClientSecretCredential; import com.microsoft.graph.models.Team; @@ -43,12 +42,14 @@ public class Graph { final Pattern orgPattern = Pattern.compile(orgMarker+"\\s*\\(\\s*([\\w"+orgSeparator+"]+)\\s*\\)"); StringTree org = new StringTree("root"); GraphServiceClient graphClient; + private boolean obfuscated = false; Graph() { // for testing only } - public Graph(ClientSecretCredential credential, String[] scopes) { + public Graph(ClientSecretCredential credential, String[] scopes, boolean obfuscated) { + this.obfuscated = obfuscated; graphClient = new GraphServiceClient(credential, scopes); } @@ -58,7 +59,7 @@ public void clear() { } public byte[] avatar(String id) throws IOException { - id = "unknown_person.jpg"; + if(obfuscated) id = "unknown_person.jpg"; try(InputStream is = graphClient.users().byUserId(id).photo().content().get()){ return is.readAllBytes(); } catch (Exception e) { @@ -87,7 +88,7 @@ public StringTree getOrg() { private String notNull(String mayBeNull) { String ret = mayBeNull == null? "" :mayBeNull; - return (ret + "...").substring(0, 3)+"..."; + return obfuscated ? (ret + "...").substring(0, 3)+"..." : ret; } private List notNull(List strings) { @@ -109,7 +110,7 @@ public Map loadTeam(String teamId, String ... roleNames) { mappedMember.put("preferredLanguage",notNull(member.getPreferredLanguage())); mappedMember.put("businessPhones",notNull(member.getBusinessPhones())); mappedMember.put("department",notNull(member.getDepartment())); - //mappedMember.put("userKey",notNull(member.getMailNickname())); + if(!obfuscated) mappedMember.put("userKey",notNull(member.getMailNickname())); mappedMember.put("usageLocation",notNull(member.getUsageLocation())); ((Set) mappedMember.computeIfAbsent("roles",(ignored)-> new TreeSet<>())).add(roleName); }); @@ -178,5 +179,10 @@ StringTree parseOrg(String description) { public Map> getRoleSchemes() { return rolesSchemes.entrySet().stream().filter(e->e.getValue().size()>1).collect(toMap(Entry::getKey,Entry::getValue)); } + + public Graph withObfuscation(boolean obfuscated) { + this.obfuscated = obfuscated; + return this; + } }