Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/prerna/auth/utils/AbstractSecurityUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -929,12 +929,12 @@ public static void initialize() throws Exception {
// Type and cost are the main questions -
boolean projectExists = queryUtil.tableExists(conn, "PROJECT", database, schema);
colNames = new String[] { "PROJECTID", "PROJECTNAME", "PROJECTDISPLAYNAME", "GLOBAL", "DISCOVERABLE",
"CREATEDBY", "CREATEDBYTYPE", "DATECREATED", "DATELASTEDITED", "TYPE", "COST", "CATALOGNAME",
"PORTALPUBLISHED", "PORTALPUBLISHEDUSER", "PORTALPUBLISHEDTYPE", "REACTORSCOMPILED",
"IS_TEMPLATE", "CREATEDBY", "CREATEDBYTYPE", "DATECREATED", "DATELASTEDITED", "TYPE", "COST",
"CATALOGNAME", "PORTALPUBLISHED", "PORTALPUBLISHEDUSER", "PORTALPUBLISHEDTYPE", "REACTORSCOMPILED",
"REACTORSCOMPILEDUSER", "REACTORSCOMPILEDTYPE" };
types = new String[] { VARCHAR_255, VARCHAR_255, VARCHAR_255, BOOLEAN_DATATYPE_NAME, BOOLEAN_DATATYPE_NAME,
VARCHAR_255, VARCHAR_255, TIMESTAMP_DATATYPE_NAME, TIMESTAMP_DATATYPE_NAME, VARCHAR_255,
VARCHAR_255, VARCHAR_255, TIMESTAMP_DATATYPE_NAME, VARCHAR_255, VARCHAR_255,
BOOLEAN_DATATYPE_NAME, VARCHAR_255, VARCHAR_255, TIMESTAMP_DATATYPE_NAME, TIMESTAMP_DATATYPE_NAME,
VARCHAR_255, VARCHAR_255, VARCHAR_255, TIMESTAMP_DATATYPE_NAME, VARCHAR_255, VARCHAR_255,
TIMESTAMP_DATATYPE_NAME, VARCHAR_255, VARCHAR_255 };
if (allowIfExistsTable) {
String sql = queryUtil.createTableIfNotExists("PROJECT", colNames, types);
Expand Down Expand Up @@ -966,6 +966,7 @@ public static void initialize() throws Exception {
// backfill display name from canonical name for existing rows
securityDb.insertData(
"UPDATE PROJECT SET PROJECTDISPLAYNAME = PROJECTNAME WHERE PROJECTDISPLAYNAME IS NULL OR PROJECTDISPLAYNAME = ''");
securityDb.insertData("UPDATE PROJECT SET IS_TEMPLATE = FALSE WHERE IS_TEMPLATE IS NULL");
}
if (allowIfExistsIndexs) {
String sql = queryUtil.createIndexIfNotExists("PROJECT_GLOBAL_INDEX", "PROJECT", "GLOBAL");
Expand Down
33 changes: 33 additions & 0 deletions src/prerna/auth/utils/SecurityAdminUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,7 @@ public List<Map<String, Object>> getAllProjectSettings(List<String> projectFilte
qs.addSelector(new QueryColumnSelector(projectPrefix + "COST", "project_cost"));
qs.addSelector(new QueryColumnSelector(projectPrefix + "GLOBAL", "project_global"));
qs.addSelector(new QueryColumnSelector(projectPrefix + "DISCOVERABLE", "project_discoverable"));
qs.addSelector(new QueryColumnSelector(projectPrefix + "IS_TEMPLATE", "project_is_template"));
qs.addSelector(new QueryColumnSelector(projectPrefix + "CATALOGNAME", "project_catalog_name"));
qs.addSelector(new QueryColumnSelector(projectPrefix + "CREATEDBY", "project_created_by"));
qs.addSelector(new QueryColumnSelector(projectPrefix + "CREATEDBYTYPE", "project_created_by_type"));
Expand Down Expand Up @@ -1689,6 +1690,38 @@ public boolean setProjectGlobal(String projectId, boolean global) {
return true;
}

/**
* Set whether viewers may clone a project as a template.
*
* @param projectId project identifier
* @param isTemplate whether the project is a template
* @return {@code true} when the flag is updated
*/
public boolean setProjectTemplate(String projectId, boolean isTemplate) {
IRDBMSEngine securityDb = SystemEngineRegistry.getSecurityDb();
PreparedStatement ps = null;
try {
ps = securityDb.getPreparedStatement("UPDATE PROJECT SET IS_TEMPLATE=? WHERE PROJECTID=?");
ps.setBoolean(1, isTemplate);
ps.setString(2, projectId);
int updatedRows = ps.executeUpdate();
if (updatedRows != 1) {
throw new IllegalArgumentException("Project does not exist");
}
if (!ps.getConnection().getAutoCommit()) {
ps.getConnection().commit();
}
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
classLogger.error("Failed to update project template setting", e);
throw new IllegalArgumentException("An error occurred setting the project template flag", e);
} finally {
ConnectionUtils.closeAllConnectionsIfPooling(securityDb, ps);
}
return true;
}

/**
* Set if the project is discoverable to all users on this instance
*
Expand Down
4 changes: 2 additions & 2 deletions src/prerna/auth/utils/SecurityOwlCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ public class SecurityOwlCreator extends AbstractOwlCreator {
static {
relationshipsRequired.add(
new String[] { "GITHUB_APP", "GITHUB_PROJECT_LINK", "GITHUB_APP.APP_ID.GITHUB_PROJECT_LINK.APP_ID" });
relationshipsRequired.add(
new String[] { "ENGINE", "MODELMETADATA", "ENGINE.ENGINEID.MODELMETADATA.ENGINEID" });
relationshipsRequired.add(new String[] { "ENGINE", "MODELMETADATA", "ENGINE.ENGINEID.MODELMETADATA.ENGINEID" });
}

public SecurityOwlCreator(AbstractSqlQueryUtil queryUtil) {
Expand Down Expand Up @@ -131,6 +130,7 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) {
Pair.with("PROJECTDISPLAYNAME", VARCHAR_255),
Pair.with("GLOBAL", BOOLEAN_DATATYPE_NAME),
Pair.with("DISCOVERABLE", BOOLEAN_DATATYPE_NAME),
Pair.with("IS_TEMPLATE", BOOLEAN_DATATYPE_NAME),
Pair.with("TYPE", VARCHAR_255),
Pair.with("COST", VARCHAR_255),
Pair.with("CATALOGNAME", VARCHAR_255),
Expand Down
94 changes: 92 additions & 2 deletions src/prerna/auth/utils/SecurityProjectUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,8 @@ public static void addProject(String projectId, String projectName, String proje
public static void addProject(String projectId, String projectName, String projectDisplayName, String projectType,
String projectCost, boolean global, User user) {
IRDBMSEngine securityDb = SystemEngineRegistry.getSecurityDb();
String query = "INSERT INTO PROJECT (PROJECTID, PROJECTNAME, TYPE, COST, GLOBAL, DISCOVERABLE, CREATEDBY, CREATEDBYTYPE, DATECREATED, DATELASTEDITED, PROJECTDISPLAYNAME) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?)";
String query = "INSERT INTO PROJECT (PROJECTID, PROJECTNAME, TYPE, COST, GLOBAL, DISCOVERABLE, IS_TEMPLATE, CREATEDBY, CREATEDBYTYPE, DATECREATED, DATELASTEDITED, PROJECTDISPLAYNAME) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";

PreparedStatement ps = null;
try {
Expand All @@ -478,6 +478,7 @@ public static void addProject(String projectId, String projectName, String proje
ps.setString(parameterIndex++, projectCost);
ps.setBoolean(parameterIndex++, global);
ps.setBoolean(parameterIndex++, false);
ps.setBoolean(parameterIndex++, false);
if (user != null) {
AuthProvider ap = user.getPrimaryLogin();
AccessToken token = user.getAccessToken(ap);
Expand Down Expand Up @@ -1136,6 +1137,39 @@ public static boolean projectIsGlobal(String projectId) {
return false;
}

/**
* Determine whether a project has been explicitly enabled as a template.
*
* @param projectId project identifier
* @return {@code true} only when the persisted template flag is true
*/
public static boolean projectIsTemplate(String projectId) {
IRDBMSEngine securityDb = SystemEngineRegistry.getSecurityDb();
SelectQueryStruct qs = new SelectQueryStruct();
qs.addSelector(new QueryColumnSelector("PROJECT__PROJECTID"));
qs.addExplicitFilter(
SimpleQueryFilter.makeColToValFilter("PROJECT__IS_TEMPLATE", "==", true, PixelDataType.BOOLEAN));
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("PROJECT__PROJECTID", "==", projectId));
try (IRawSelectWrapper wrapper = WrapperManager.getInstance().getRawWrapper(securityDb, qs)) {
return wrapper.hasNext();
} catch (Exception e) {
classLogger.error("Failed to determine whether project is enabled as a template", e);
return false;
}
}

/**
* Determine whether a user may clone a project. The user must be able to view
* the project and the owner must have explicitly enabled it as a template.
*
* @param user current user
* @param projectId project identifier
* @return whether the user may clone the project
*/
public static boolean userCanCloneProject(User user, String projectId) {
return userCanViewProject(user, projectId) && projectIsTemplate(projectId);
}

/**
*
* @param projectId
Expand Down Expand Up @@ -1784,6 +1818,46 @@ public static boolean setProjectGlobal(User user, String projectId, boolean glob
return true;
}

/**
* Set whether viewers may clone a project as a template.
*
* @param user current user
* @param projectId project identifier
* @param isTemplate whether the project is a template
* @return {@code true} when the flag is updated
* @throws IllegalAccessException when the user is not the project owner
*/
public static boolean setProjectTemplate(User user, String projectId, boolean isTemplate)
throws IllegalAccessException {
IRDBMSEngine securityDb = SystemEngineRegistry.getSecurityDb();
if (!SecurityUserProjectUtils.userIsOwner(user, projectId)) {
throw new IllegalAccessException(
"The user doesn't have permission to set this project as a template. Only the owner or an admin can perform this action.");
}

PreparedStatement ps = null;
try {
ps = securityDb.getPreparedStatement("UPDATE PROJECT SET IS_TEMPLATE=? WHERE PROJECTID=?");
ps.setBoolean(1, isTemplate);
ps.setString(2, projectId);
int updatedRows = ps.executeUpdate();
if (updatedRows != 1) {
throw new IllegalArgumentException("Project does not exist");
}
if (!ps.getConnection().getAutoCommit()) {
ps.getConnection().commit();
}
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
classLogger.error("Failed to update project template setting", e);
throw new IllegalArgumentException("An error occurred setting the project template flag", e);
} finally {
ConnectionUtils.closeAllConnectionsIfPooling(securityDb, ps);
}
return true;
}

/**
* Set a project and all its insights to be global
*
Expand Down Expand Up @@ -3184,6 +3258,14 @@ public static List<Map<String, Object>> getUserProjectList(User user, List<Strin
List<String> projectIdFilters, boolean favoritesOnly, Map<String, Object> projectMetadataFilter,
List<Integer> permissionFilters, String searchTerm, String limit, String offset,
Map<String, String> sortFields) {
return getUserProjectList(user, projectTypes, projectIdFilters, favoritesOnly, projectMetadataFilter,
permissionFilters, searchTerm, limit, offset, sortFields, false);
}

public static List<Map<String, Object>> getUserProjectList(User user, List<String> projectTypes,
List<String> projectIdFilters, boolean favoritesOnly, Map<String, Object> projectMetadataFilter,
List<Integer> permissionFilters, String searchTerm, String limit, String offset,
Map<String, String> sortFields, boolean onlyTemplates) {
IRDBMSEngine securityDb = SystemEngineRegistry.getSecurityDb();

boolean hasSearchTerm = searchTerm != null && !(searchTerm = searchTerm.trim()).isEmpty();
Expand All @@ -3201,6 +3283,7 @@ public static List<Map<String, Object>> getUserProjectList(User user, List<Strin
qs1.addSelector(new QueryColumnSelector(projectPrefix + "COST", "project_cost"));
qs1.addSelector(new QueryColumnSelector(projectPrefix + "GLOBAL", "project_global"));
qs1.addSelector(new QueryColumnSelector(projectPrefix + "DISCOVERABLE", "project_discoverable"));
qs1.addSelector(new QueryColumnSelector(projectPrefix + "IS_TEMPLATE", "project_is_template"));
qs1.addSelector(new QueryColumnSelector(projectPrefix + "CATALOGNAME", "project_catalog_name"));
qs1.addSelector(new QueryColumnSelector(projectPrefix + "CREATEDBY", "project_created_by"));
qs1.addSelector(new QueryColumnSelector(projectPrefix + "CREATEDBYTYPE", "project_created_by_type"));
Expand Down Expand Up @@ -3369,6 +3452,11 @@ public static List<Map<String, Object>> getUserProjectList(User user, List<Strin
qs1.addExplicitFilter(SimpleQueryFilter.makeColToValFilter(projectPrefix + "TYPE", "==", projectTypes));
}

if (onlyTemplates) {
qs1.addExplicitFilter(SimpleQueryFilter.makeColToValFilter(projectPrefix + "IS_TEMPLATE", "==", true,
PixelDataType.BOOLEAN));
}

if (projectIdFilters != null && !projectIdFilters.isEmpty()) {
qs1.addExplicitFilter(
SimpleQueryFilter.makeColToValFilter(projectPrefix + "PROJECTID", "==", projectIdFilters));
Expand Down Expand Up @@ -3623,6 +3711,7 @@ public static List<Map<String, Object>> getUserProjectList(User user, String pro
qs.addSelector(new QueryColumnSelector("PROJECT__COST", "project_cost"));
qs.addSelector(new QueryColumnSelector("PROJECT__GLOBAL", "project_global"));
qs.addSelector(new QueryColumnSelector("PROJECT__DISCOVERABLE", "project_discoverable"));
qs.addSelector(new QueryColumnSelector("PROJECT__IS_TEMPLATE", "project_is_template"));
qs.addSelector(new QueryColumnSelector("PROJECT__CATALOGNAME", "project_catalog_name"));
qs.addSelector(new QueryColumnSelector("PROJECT__CREATEDBY", "project_created_by"));
qs.addSelector(new QueryColumnSelector("PROJECT__CREATEDBYTYPE", "project_created_by_type"));
Expand Down Expand Up @@ -3709,6 +3798,7 @@ public static List<Map<String, Object>> getDiscoverableProjectList(String projec
qs.addSelector(new QueryColumnSelector("PROJECT__COST", "project_cost"));
qs.addSelector(new QueryColumnSelector("PROJECT__GLOBAL", "project_global"));
qs.addSelector(new QueryColumnSelector("PROJECT__DISCOVERABLE", "project_discoverable"));
qs.addSelector(new QueryColumnSelector("PROJECT__IS_TEMPLATE", "project_is_template"));
qs.addSelector(new QueryColumnSelector("PROJECT__CATALOGNAME", "project_catalog_name"));
qs.addSelector(new QueryColumnSelector("PROJECT__CREATEDBY", "project_created_by"));
qs.addSelector(new QueryColumnSelector("PROJECT__CREATEDBYTYPE", "project_created_by_type"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public String getReactorDescription() {

Returns a single map (PROJECT_INFO/CUSTOM_DATA_STRUCTURE) containing:
Core project fields: project_id, project_name, project_display_name, project_type, project_cost,
project_global, project_discoverable, project_catalog_name, project_created_by, project_created_by_type,
project_global, project_discoverable, project_is_template, project_catalog_name, project_created_by, project_created_by_type,
project_date_created, project_date_last_edited, low_project_name.
Portal fields: project_has_portal, project_portal_name, project_portal_published_date, project_published_user,
project_published_user_type, project_reactors_compiled_date, project_reactors_compiled_user,
Expand All @@ -158,4 +158,4 @@ protected String getDescriptionForKey(String key) {
return super.getDescriptionForKey(key);
}

}
}
10 changes: 7 additions & 3 deletions src/prerna/reactor/agent/skill/CloneSkillReactor.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
* <ul>
* <li>Every file under the source's skill content folder (SKILL.md + any
* helpers).</li>
* <li>The description, re-synthesized into the clone's SKILL.md frontmatter.</li>
* <li>The description, re-synthesized into the clone's SKILL.md
* frontmatter.</li>
* </ul>
*
* <p>
Expand All @@ -82,8 +83,8 @@
* </ul>
*
* <p>
* Authorization: caller must be able to view the source skill-project
* ({@link SecurityProjectUtils#userCanViewProject}).
* Authorization: caller must be able to view the source skill-project and the
* project must be explicitly enabled as a template.
*/
public class CloneSkillReactor extends AbstractReactor {

Expand Down Expand Up @@ -112,6 +113,9 @@ public NounMetadata execute() {
throw new IllegalArgumentException(
"Skill " + sourceSkillId + " does not exist or user does not have permission to view it");
}
if (!SecurityProjectUtils.userCanCloneProject(user, sourceSkillId)) {
throw new IllegalArgumentException("This skill is not enabled as a template and cannot be cloned.");
}

if (!SkillProjects.isSkillProject(sourceSkillId)) {
throw new IllegalArgumentException("Skill not found: " + sourceSkillId);
Expand Down
23 changes: 8 additions & 15 deletions src/prerna/reactor/project/CreateAppFromTemplateReactor.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@

import prerna.auth.User;
import prerna.auth.utils.SecurityProjectUtils;
import prerna.engine.impl.model.inferencetracking.ModelInferenceLogsUtils;
import prerna.cluster.util.ClusterUtil;
import prerna.engine.impl.model.inferencetracking.ModelInferenceLogsUtils;
import prerna.project.api.IProject;
import prerna.project.impl.ProjectHelper;
import prerna.reactor.AbstractReactor;
Expand All @@ -66,7 +66,8 @@ public class CreateAppFromTemplateReactor extends AbstractReactor {

private static final String CLASS_NAME = CreateAppFromTemplateReactor.class.getName();

// PROJECTMETA tag marking a project as platform-managed; never copied onto a clone
// PROJECTMETA tag marking a project as platform-managed;
// never copied onto a clone
private static final String SYSTEM_TAG = "SYSTEM";

/*
Expand All @@ -80,11 +81,6 @@ public CreateAppFromTemplateReactor() {
ReactorKeysEnum.GLOBAL.getKey(), ReactorKeysEnum.PROVIDER.getKey(), ReactorKeysEnum.URL.getKey() };
}

// Allow viewer cloning for agents and skills but not apps
static boolean canCloneProject(IProject.PROJECT_TYPE projectType, boolean canEdit) {
return projectType == IProject.PROJECT_TYPE.WORKSPACE || projectType == IProject.PROJECT_TYPE.SKILL || canEdit;
}

@Override
public NounMetadata execute() {
Logger logger = getLogger(CLASS_NAME);
Expand All @@ -108,10 +104,8 @@ public NounMetadata execute() {

// Use the template to populate the parameters needed to create the new project
IProject.PROJECT_TYPE projectEnumType = templateProject.getProjectType();
boolean canEdit = SecurityProjectUtils.userCanEditProject(this.insight.getUser(), projectTemplateId);
if (!canCloneProject(projectEnumType, canEdit)) {
throw new IllegalArgumentException(
"Only owners and editors can clone regular projects. Agents and skills can be cloned with read-only access.");
if (!SecurityProjectUtils.userCanCloneProject(this.insight.getUser(), projectTemplateId)) {
throw new IllegalArgumentException("This project is not enabled as a template and cannot be cloned.");
}

// Create new project
Expand Down Expand Up @@ -163,7 +157,6 @@ public NounMetadata execute() {
+ e.getMessage());
}


if (IProject.PROJECT_TYPE.WORKSPACE == projectEnumType) {
try {
User user = this.insight.getUser();
Expand Down Expand Up @@ -198,8 +191,7 @@ public NounMetadata execute() {
}

SecurityProjectUtils.updateProjectDependencies(user, newProjectId, dependencyList);
ModelInferenceLogsUtils.createNewWorkspaceEntry(newProjectId,
user.getPrimaryLoginToken().getId(),
ModelInferenceLogsUtils.createNewWorkspaceEntry(newProjectId, user.getPrimaryLoginToken().getId(),
newProjectName, sourceDescription, sourceSystemPrompt, clonedResources);
if (sourceConfigJson != null) {
ModelInferenceLogsUtils.updateWorkspaceConfigJson(newProjectId, sourceConfigJson);
Expand All @@ -226,7 +218,8 @@ public NounMetadata execute() {
SecurityProjectUtils.updateProjectMetadata(newProjectId, tagUpdate);
}
} catch (Exception e) {
classLogger.error("Failed to clone workspace inference log entry from template '{}' to new project '{}'.",
classLogger.error(
"Failed to clone workspace inference log entry from template '{}' to new project '{}'.",
projectTemplateId, newProject.getProjectId(), e);
try {
newProject.delete();
Expand Down
Loading
Loading