UNCLASSIFIED

You need to sign in or sign up before continuing.
ApiResource.java 12.5 KB
Newer Older
project_646_bot's avatar
project_646_bot committed
1 2
package dod.p1.keycloak.api;

3 4 5 6 7
import dod.p1.keycloak.api.cache.GroupMembershipCache;
import dod.p1.keycloak.api.cache.UsersCache;
import dod.p1.keycloak.api.responses.*;
import dod.p1.keycloak.common.ApiEndpointConfig;
import dod.p1.keycloak.common.CommonConfig;
project_646_bot's avatar
project_646_bot committed
8 9
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
10
import org.keycloak.connections.jpa.JpaConnectionProvider;
11
import org.keycloak.models.Constants;
project_646_bot's avatar
project_646_bot committed
12 13
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
14 15
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
project_646_bot's avatar
project_646_bot committed
16
import org.keycloak.models.RealmModel;
17
import org.keycloak.models.jpa.entities.GroupEntity;
18 19
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
project_646_bot's avatar
project_646_bot committed
20 21 22 23
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;

24 25
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
project_646_bot's avatar
project_646_bot committed
26 27 28 29
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.*;
30
import java.util.stream.Stream;
project_646_bot's avatar
project_646_bot committed
31 32 33 34 35 36

public class ApiResource {
    private static final Logger LOGGER = Logger.getLogger(ApiResource.class);

    private final AuthenticationManager.AuthResult auth;
    private final Map<String, ApiEndpointConfig> apiEndpoints;
37 38 39
    private final GroupMembershipCache groupMembershipCache;
    private final UsersCache usersCache;
    private final KeycloakSession session;
project_646_bot's avatar
project_646_bot committed
40

41
    public ApiResource(KeycloakSession session, GroupMembershipCache groupMembershipCache, UsersCache usersCache) {
42 43
        if (!groupMembershipCache.isInitialized()) {
            LOGGER.info("groupMembershipCache is not initialized, initializing..");
44 45
            groupMembershipCache.init(session);
        } else {
46
            LOGGER.info("groupMembershipCache is initialized.");
47
        }
48 49
        if (!usersCache.isInitialized()) {
            LOGGER.info("usersCache is not initialized, initializing..");
50 51
            usersCache.init(session);
        } else {
52
            LOGGER.info("usersCache is initialized.");
53 54 55
        }
        this.groupMembershipCache = groupMembershipCache;
        this.usersCache = usersCache;
project_646_bot's avatar
project_646_bot committed
56
        this.session = session;
57

project_646_bot's avatar
project_646_bot committed
58 59 60
        AppAuthManager appAuthManager = new AppAuthManager();
        //bearer token needs to come from a user that exists in the baby-yoda realm for authentication to work
        this.auth = appAuthManager.authenticateBearerToken(session);
61 62 63
        RealmModel realm = session.getContext().getRealm();
        CommonConfig commonConfig = CommonConfig.getInstance(realm);
        this.apiEndpoints = commonConfig.getApiConfigs();
project_646_bot's avatar
project_646_bot committed
64 65 66 67
    }

    /**
     * Build a list of group response for the given IL group id.
68 69 70 71
     * 
     * Use the `first` and `max` parameters to page through results.
     *      * `first` indicates the index of the first group result to return.
     *      * `max` indicates the total "page" size - if `first` is set and `max` is not, a default page size of 100 will be used.
project_646_bot's avatar
project_646_bot committed
72
     */
73
    public static GroupsResponse buildGroupsModelForIlGroup(KeycloakSession session, ApiEndpointConfig endpointConfig,
74
                                                            GroupMembershipCache groupMembershipCache, UsersCache usersCache, int first, int max) {
75 76
        if (endpointConfig == null) {
            throw new IllegalArgumentException("endpoint config cannot be null.");
project_646_bot's avatar
project_646_bot committed
77 78
        }

79
        // initialize user output list
80
        Set<UserResponse> userResponses = new HashSet<>();
81 82

        // fetch all users for group
83
        Set<String> ilUserIds = groupMembershipCache.getMemberUserIds(endpointConfig.getAuthorizedGroup().getId());
project_646_bot's avatar
project_646_bot committed
84

85
        Map<String, GroupResponseBuilder> productToGroupInfoMap = new HashMap<>();
86

87 88 89 90
        //set a default max if none is provided
        if(first > 0 && max == 0) {
            max = Constants.DEFAULT_MAX_RESULTS; // default = 100
        }
91

92
        //get relevent group IDs with a custom query
93
        String[] releventGroupIds = getProductGroupIds(session, groupMembershipCache, endpointConfig.getEnvironment(), first, max);
94

95 96 97
        //get the current realm
        RealmModel realm = session.getContext().getRealm();

98 99
        //for each product group
        for(String productGroupId : releventGroupIds) {
100
            //get the group
101 102 103
            GroupModel productGroup = session.realms().getGroupById(productGroupId, realm);
            if(productGroup == null) {
                LOGGER.infof("null GroupModel returned for ID %s - skipping", productGroupId);
104 105 106
                continue;
            }

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
            // get the group's children
            Set<GroupModel> subGroups = productGroup.getSubGroups();

            // for each product sub-group (should only be `developer` and `collaborator`) - add the appropriate users
            for (GroupModel roleGroup : subGroups) {
                // double check that this is a "role" / leaf node 
                if (!(roleGroup.getName().equals("developer") || roleGroup.getName().equals("collaborator"))) {
                    LOGGER.errorf("found a non-relevent group (%s) which is not a `developer` or `collaborator` - this shouldn't happen", roleGroup.getId());
                    continue;
                }

                // get the mattermost team names for the group from the cache
                String roleGroupPath = ModelToRepresentation.buildGroupPath(roleGroup);
                Set<String> mattermostTeamNames = endpointConfig.getMattermostTeamNames(roleGroupPath);
                if (mattermostTeamNames.isEmpty()) {
                    //LOGGER.infof("Skipping because no mattermost team name: ", groupPath);
                    LOGGER.errorf("found a non-relevent group (%s) with no mattermost teamnames in the cache - this shouldn't happen", roleGroup.getId());
                    continue;
                }

                // build the group path and parse product and role
                String productPath = roleGroupPath.substring(0, roleGroupPath.length() - roleGroup.getName().length() - 1);
                String[] productPathSplits = roleGroupPath.split("/");
                String productName = productPathSplits[productPathSplits.length - 2];
                String role = roleGroup.getName();

                Set<String> memberIds = groupMembershipCache.getMemberUserIds(roleGroup.getId());
                GroupResponseBuilder groupInfo = productToGroupInfoMap.computeIfAbsent(productPath,
                        path -> new GroupResponseBuilder(productName, path, new HashMap<>(), mattermostTeamNames));
                if (memberIds != null) {
                    memberIds.stream().filter(ilUserIds::contains).forEach(u -> {
                        UserResponse user = usersCache.getUserById(u);
                        if (user != null) {
                            Set<String> roles = groupInfo.getUserToRolesMap().computeIfAbsent(user.getUsername(), n -> new HashSet<>());
                            roles.add(role);
                            userResponses.add(user);
                        }
                    });
                }
146
            }
project_646_bot's avatar
project_646_bot committed
147
        }
148
        LOGGER.info("Done building map for all groups ..");
project_646_bot's avatar
project_646_bot committed
149

150 151 152
        List<GroupResponse> groupResponses = new ArrayList<>();
        for(Map.Entry<String, GroupResponseBuilder> entry : productToGroupInfoMap.entrySet()) {
            GroupResponseBuilder groupInfo = entry.getValue();
project_646_bot's avatar
project_646_bot committed
153
            Set<GroupMember> groupMembers = new HashSet<>();
154 155 156 157 158
            for(Map.Entry<String, Set<String>> entry1 : groupInfo.getUserToRolesMap().entrySet()) {
                groupMembers.add(new GroupMember(entry1.getKey(), entry1.getValue()));
            }
            GroupResponse groupResponse = new GroupResponse(groupInfo.getName(), groupInfo.getPath(), groupMembers, groupInfo.getMattermostTeams());
            groupResponses.add(groupResponse);
project_646_bot's avatar
project_646_bot committed
159
        }
160
        return new GroupsResponse(groupResponses, userResponses);
project_646_bot's avatar
project_646_bot committed
161 162
    }

163 164 165
    // Uses a custom query to find all relevent product group IDs for the specified page.
    // Relevent groups must have a `developer` or `collaborator` child node which belong to the mmGroupId cache (i.e. have a path defined in the config for the environment)
    private static String[] getProductGroupIds(KeycloakSession session,GroupMembershipCache groupCache, String env, int first, int max) {
166 167 168
        KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
        final List<String> releventGroupIds = new ArrayList<>();

169
        //get mattermost group IDs and build a SQL formatted list in the format '('group1_ID','group2_ID',...)'
170 171 172 173
        String[] mmGroupIds = groupCache.getAllMattermostGroups(env);
        if (mmGroupIds.length == 0) {
            return mmGroupIds;
        }
174 175 176 177 178
        String mmGroupIdCSV = "'" + String.join("','", mmGroupIds) + "'";

        // build a query to page through "valid" product groups where the group has a `developer` or `collaborator` child group which is listed in mattermost group IDs (i.e. path is defined in the config for the environment)
        String queryString = String.format("SELECT group.id FROM GroupEntity group WHERE group.id IN (%s)",
            String.format("SELECT g.parentId FROM GroupEntity g WHERE g.name IN ('developer', 'collaborator') AND g.id IN (%s)", mmGroupIdCSV));
179 180 181 182 183 184 185
        
        // make a keycloak transaction
        KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
            @Override
            public void run(KeycloakSession session) {
                // query for group IDs for groups with the name `developer` or `collaborator` with mattermost teams
                EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
186 187
                LOGGER.infof("Querying for product groups.",  mmGroupIds.length);
                try {
188 189 190 191 192 193 194
                    TypedQuery<String> groupQuery = em.createQuery(queryString, String.class);
                    if(first > 0) {
                        groupQuery.setFirstResult(first);
                    }
                    if(max > 0) {
                        groupQuery.setMaxResults(max);
                    }
195 196 197 198
                    
                    // add results to the output set and log size
                    groupQuery.getResultStream().forEach(id -> releventGroupIds.add(id));
                } catch (Exception e) {
199
                    LOGGER.error("error performing product group query: " + e.getMessage());
200
                }
Andrew Blanchard's avatar
Andrew Blanchard committed
201
                LOGGER.infof("found %d relevent groups", releventGroupIds.size());
202 203 204 205 206
            }
        });
        return releventGroupIds.toArray(String[]::new);
    }

project_646_bot's avatar
project_646_bot committed
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    /**
     * Validate user is authorized to access the endpoint. Only particular api user is allowed to access each realm and they must authenticate successfully.
     * @param auth the auth result
     * @param allowedUserName the allowed user name to access the endpoint
     */
    public static void validateUserAuthorization(AuthenticationManager.AuthResult auth, String allowedUserName) {
        if (auth == null || !auth.getUser().getUsername().equals(allowedUserName)) {
            Response.status(Response.Status.BAD_REQUEST).build();
            LOGGER.info("Access to groups API is not authorized");
            throw new NotAuthorizedException("Not Authorized");
        }
    }

    @GET
    @NoCache
    @Produces(MediaType.APPLICATION_JSON)
    public GroupsResponse getGroups(
224 225 226
            @QueryParam("env") String env,
            @QueryParam("first") int first,
            @QueryParam("max") int max) {
227 228 229 230 231 232 233 234
        // check input params
        if(first < 0) {
            throw new BadRequestException("Query parameter 'first' cannot be less than 0");
        }
        if(max < 0) {
            throw new BadRequestException("Query paramter 'max' cannot be less than 0.");
        }

project_646_bot's avatar
project_646_bot committed
235
        LOGGER.info("Processing groups api request for " + env);
236
        LOGGER.infof("{first: %d, max %d}", first, max);
project_646_bot's avatar
project_646_bot committed
237 238 239 240 241 242
        ApiEndpointConfig endpointConfig = this.apiEndpoints.get(env);
        if (endpointConfig == null) {
            throw new BadRequestException(env + " is not a valid value for the 'env' query param.");
        }
        validateUserAuthorization(auth, endpointConfig.getAllowedUser());

243
        GroupsResponse groups = buildGroupsModelForIlGroup(this.session, endpointConfig, this.groupMembershipCache, this.usersCache, first, max);
244 245
        LOGGER.info("total users: " + groups.getUsers().size());
        LOGGER.info("total groups: " + groups.getGroups().size());
project_646_bot's avatar
project_646_bot committed
246 247 248 249
        LOGGER.info("Done processing groups api request for " + env);
        return groups;
    }
}