Create a Service User with Default Service User Objects
Once your ALTR account is created, use ALTR’s stored procedure to create your service user and grant privileges.
Note
If your service username, role and warehouse are changed from the default objects (PC_ALTR_USER, PC_ALTR_ROLE and/or PC_ALTR_WAREHOUSE, or ALTR_SERVICE_USER, ALTR_SERVICE_ROLE and ALTR_SERVICE_WAREHOUSE), this stored procedure will not work. Instead, run the stored procedure with custom service user objects. If you need to create a custom service user, we have very specific instructions. Learn more.
To create a service user with the default service user objects and grant privileges:
Create a database with a well-known name (our recommendation is PC_ALTR_DB) that will house ALTR’s stored procedure.
Create the service user named ALTR_SERVICE_USER with a TYPE of LEGACY_SERVICE. Later, you will execute a stored procedure that creates the default role (ALTR_SERVICE_ROLE) and the default warehouse (ALTR_SERVICE_WH) if they do not already exist.
Note
Starting 09/30/24, all new Snowflake accounts by default require multi-factor authorization (MFA) to log into Snowflake. Set the TYPE parameter to LEGACY_SERVICE on ALTR's service user to ensure ALTR can authenticate with Snowflake. Learn more. This parameter was automatically set if you signed up for ALTR via Snowflake Partner Connect after October 2024. ALTR recommends setting a network policy on the service user to prevent unauthorized access. Learn more.
Run the following SnowSQL to create ALTR_SERVICE_USER. Make sure to set and remember a strong password.
CREATE USER IF NOT EXISTS ALTR_SERVICE_USER TYPE = LEGACY_SERVICE password = "<Password>";
Run the following SnowSQL in the database to create the ALTR_SETUP_SERVICE_ACCOUNT stored procedure. You should take note of which database you create the procedure in.
Note
When you run the stored procedure, set the IS_PARTNER_CONNECT to FALSE, unless your service user was automatically created by Snowflake Partner Connect (PC_ALTR_USER).
CREATE OR REPLACE PROCEDURE SETUP_ALTR_SERVICE_ACCOUNT(IS_PARTNER_CONNECT BOOLEAN) RETURNS STRING LANGUAGE JAVASCRIPT VOLATILE EXECUTE AS CALLER AS $$ // ******************************************** // Stored Procedure Caller Report // ******************************************** function AltrServiceAccountException(message) { this.message = message; } function StoredProcedureReport() { this.isSuccess = true; this.successMessages = []; this.failMessages = []; this.unknownDBList = []; this.skippedDBList = []; this.fail = function(message) { this.isSuccess = false; this.failMessages.push(message); } this.success = function(message) { this.successMessages.push(message); } this.unknownDB = function(message) { this.unknownDBList.push(message); } this.skipped = function(message) { this.skippedDBList.push(message); } this.callerReport = function() { let report = ''; function appendMessages(prefix, messages) { const totalPrefix = '\n[' + prefix + ']: '; for (const message of messages) { report += totalPrefix + message; } } if (this.isSuccess) { report = 'SUCCEEDED!'; } else { report = 'FAILED!'; appendMessages('FAILURE', this.failMessages); } appendMessages('SUCCESS', this.successMessages); appendMessages('SKIPPED', this.skippedDBList); appendMessages('UKNOWN OBJECT FOUND', this.unknownDBList); return report; } } var RUN_AS_ALTR = false; const storedProcedureReport = new StoredProcedureReport(); // ******************************************** // Utilities // ******************************************** function isNullOrEmpty(str) { return typeof('string') !== typeof(str) || str.length < 1; } function tryGetColumnValueAsString(resultSet, columnName) { try { return resultSet.getColumnValueAsString(columnName); } catch (err) { return null; } } // https://docs.snowflake.com/en/sql-reference/stored-procedures-api.html function execQuery(queryString) { return snowflake.execute({sqlText:queryString}); } function execStatement(queryString, handleError) { try { const resultSet = execQuery(queryString); storedProcedureReport.success(queryString); return true; } catch (error) { let reportFailure = true; if ('function' === typeof(handleError)) { try { reportFailure = handleError(queryString, error); } catch (error) { reportFailure = true; } } if (reportFailure) { storedProcedureReport.fail(queryString + ": " + error); } } return false; } // https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html function delimitIdentifier(identifier) { return '"' + identifier.replace(/\"/g, '""') + '"'; } function getDelimitedComment(objectType) { return "'This " + objectType + " is used by ALTR to help simplify governance and control over data in Snowflake. Please do not modify without speaking with ALTR Support.'"; } function isUknownDB(queryString, error) { const errorString = '' + error; let idx = -1; idx = errorString.indexOf('Database'); if (idx !== -1) { if (errorString.includes(' does not exist or not authorized.')) { storedProcedureReport.unknownDB(errorString.substring(idx)); return false; } } return true; } // ******************************************** // Main functionality // ******************************************** function permissionRole(delimitedRoleName) { // ******************************************** // Grant account-level privileges on the role // ******************************************** let targetRoleExists = true; execStatement('GRANT CREATE DATABASE ON ACCOUNT TO ROLE ' + delimitedRoleName, function(queryString, error) { const errorString = '' + error; if (errorString.includes('Role ') && errorString.includes(' does not exist or not authorized.')) { // Use this initial grant to check validity of the input role. targetRoleExists = false; let extraInfo = ''; const upperCaseTargetRoleName = ALTR_SERVICE_ROLE_NAME.toUpperCase(); if (ALTR_SERVICE_ROLE_NAME !== upperCaseTargetRoleName) { extraInfo = '. Perhaps you meant role ' + upperCaseTargetRoleName; } storedProcedureReport.fail('You called this stored procedure with a role that does not exist: ' + ALTR_SERVICE_ROLE_NAME + extraInfo + '.'); if (RUN_AS_ALTR) { throw new AltrServiceAccountException(storedProcedureReport.callerReport()); } return false; } else { // Some other error when executing the grant. Report the // error, but keep executing. return true; } }); if (!targetRoleExists) return; execStatement('GRANT APPLY MASKING POLICY ON ACCOUNT TO ROLE ' + delimitedRoleName, function(queryString, error) { // This stored procedure is written to execute on all editions of Snowflake, // including those that do not support masking policies. So, if we happen // to be on the latter, swallow this error silently and move on. return !('' + error).includes('Unsupported feature'); }); execStatement('GRANT CREATE INTEGRATION ON ACCOUNT TO ROLE ' + delimitedRoleName); execStatement('GRANT APPLY TAG ON ACCOUNT TO ROLE ' + delimitedRoleName); execStatement('GRANT APPLY ROW ACCESS POLICY ON ACCOUNT TO ROLE ' + delimitedRoleName); // ******************************************** // Grant privileges on warehouses to role // ******************************************** const warehouseNames = []; let resultSet = execQuery('SHOW WAREHOUSES'); while (resultSet.next()) { warehouseNames.push(resultSet.getColumnValueAsString('name')); } for (const warehouseName of warehouseNames) { execStatement('GRANT MONITOR ON WAREHOUSE ' + delimitIdentifier(warehouseName) + ' TO ROLE ' + delimitedRoleName); } // ******************************************** // Grant database-level privileges to role // ******************************************** const databaseNames = []; resultSet = execQuery('SHOW DATABASES'); // Do not use TERSE here; we need some of the columns it trims while (resultSet.next()) { const name = resultSet.getColumnValueAsString('name'); // Exclude TRANSIENT databases. There are no TEMPORARY databases as of this writing, // but we might as well exclude them if they every implement them. const options = tryGetColumnValueAsString(resultSet, 'options'); if (!isNullOrEmpty(options)) { const optionsUpper = options.toUpperCase(); if (optionsUpper.includes('TRANSIENT') || optionsUpper.includes('TEMPORARY')) { storedProcedureReport.skipped('Database ' + name + ' because it has options: ' + options); continue; } } // Exclude any non-standard databases (so, shared and native app). const kind = tryGetColumnValueAsString(resultSet, 'kind'); if (!isNullOrEmpty(kind) && !kind.toUpperCase().includes('STANDARD')) { storedProcedureReport.skipped('Database ' + name + ' because it is type: ' + kind); continue; } // Older versions of Snowflake did not provide us with the "kind" // column in SHOW DATABASES, so we had to check the "origin" of the // database. If there is an origin, it means the database "comes from // somewhere", and is, therefore, a SHARED database. const origin = tryGetColumnValueAsString(resultSet, 'origin'); if (!isNullOrEmpty(origin)) { storedProcedureReport.skipped('Database ' + name + ' because it has origin: ' + origin); continue; } // Just a regular database, but exclude databases with empty or null names. if (!isNullOrEmpty(name)) { databaseNames.push(name); } else { storedProcedureReport.skipped('Database ' + name + ' because it has null or empty name'); } } for (const databaseName of databaseNames) { const delimitedDatabaseName = delimitIdentifier(databaseName); let querySuccess = execStatement('GRANT USAGE ON DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); if (querySuccess === false) continue; execStatement('GRANT CREATE SCHEMA ON DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT USAGE ON FUTURE SCHEMAS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT USAGE ON ALL SCHEMAS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT SELECT ON FUTURE TABLES IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT SELECT ON ALL TABLES IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT SELECT ON FUTURE VIEWS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT SELECT ON ALL VIEWS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT SELECT ON FUTURE MATERIALIZED VIEWS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT SELECT ON ALL MATERIALIZED VIEWS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT CREATE TAG ON FUTURE SCHEMAS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT CREATE TAG ON ALL SCHEMAS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT CREATE ROW ACCESS POLICY ON FUTURE SCHEMAS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT CREATE ROW ACCESS POLICY ON ALL SCHEMAS IN DATABASE ' + delimitedDatabaseName + ' TO ROLE ' + delimitedRoleName, isUknownDB); } execStatement('CREATE DATABASE IF NOT EXISTS ALTR_DSAAS_DB'); // ownership will be accountadmin execStatement('GRANT ALL ON FUTURE SCHEMAS IN DATABASE ALTR_DSAAS_DB TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT ALL ON ALL SCHEMAS IN DATABASE ALTR_DSAAS_DB TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT ALL ON FUTURE FUNCTIONS IN DATABASE ALTR_DSAAS_DB TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT ALL ON ALL FUNCTIONS IN DATABASE ALTR_DSAAS_DB TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT ALL ON DATABASE ALTR_DSAAS_DB TO ROLE ' + delimitedRoleName, isUknownDB); execStatement('GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE ' + delimitedRoleName, isUknownDB); } function permissionUser(delimitedRoleName, delimitedUserName, delimitedWarehouseName) { // Create the role for ALTR and assign the role to SYSADMIN as recommended by // Snowflake to help with troubleshooting and debugging if necessary. execStatement('CREATE ROLE IF NOT EXISTS ' + delimitedRoleName); execStatement('ALTER ROLE ' + delimitedRoleName + ' SET COMMENT = ' + getDelimitedComment('role')); execStatement('GRANT ROLE ' + delimitedRoleName + ' TO ROLE SYSADMIN'); // Create the warehouse for ALTR and grant usage of it to the ALTR role. // Though XSMALL is the default for WAREHOUSE_SIZE and TRUE is the default // for AUTO_RESUME, we include them anyway because if the defaults of these // properties ever change, there will be serious functionality and cost concerns. execStatement('CREATE WAREHOUSE IF NOT EXISTS ' + delimitedWarehouseName + ' WITH WAREHOUSE_SIZE = XSMALL SCALING_POLICY = ECONOMY AUTO_RESUME = TRUE INITIALLY_SUSPENDED = TRUE'); execStatement('ALTER WAREHOUSE ' + delimitedWarehouseName + ' SET AUTO_SUSPEND = 30 COMMENT = ' + getDelimitedComment('warehouse')); execStatement('GRANT USAGE ON WAREHOUSE ' + delimitedWarehouseName + ' TO ROLE ' + delimitedRoleName); // Transfer ownership of the warehouse to customer SYSADMIN. They can then // assign resource monitors and other customer-specific configuration. // Though this procedure will necessarily run as ACCOUNTADMIN, Snowflake // recommeneds that database objects be owned by SYSADMIN. execStatement('GRANT OWNERSHIP ON WAREHOUSE ' + delimitedWarehouseName + ' TO ROLE SYSADMIN COPY CURRENT GRANTS'); // Permission the ALTR role. Be sure to do this after you create the // ALTR warehouse so the ALTR role gets monitor on that warehouse. permissionRole(delimitedRoleName); // Finally, assign the role to the user (the latter must already be created) // and set appropriate defaults. execStatement('GRANT ROLE ' + delimitedRoleName + ' TO USER ' + delimitedUserName); execStatement('ALTER USER ' + delimitedUserName + ' SET DEFAULT_ROLE = ' + delimitedRoleName); execStatement('ALTER USER ' + delimitedUserName + ' SET DEFAULT_WAREHOUSE = ' + delimitedWarehouseName); execStatement('ALTER USER ' + delimitedUserName + ' SET COMMENT = ' + getDelimitedComment('user')); } // ******************************************** // Main // ******************************************** let paramsResultSet = execQuery("SHOW PARAMETERS LIKE '%QUERY_TAG%' IN SESSION"); while (paramsResultSet.next()) { let queryTagStr = ''; queryTagStr = paramsResultSet.getColumnValueAsString('value'); if (!isNullOrEmpty(queryTagStr) && queryTagStr === "ALTR") { RUN_AS_ALTR = true; break; } } // This script must execute as ACCOUNTADMIN so that // we can grant the target role permissions on objects // that the target role may not even be able to see. try { execQuery("USE ROLE ACCOUNTADMIN"); } catch (error) { storedProcedureReport.fail('You must call this stored procedure with a user and role that can assume role ACCOUNTADMIN: ' + error); if (RUN_AS_ALTR) { throw new AltrServiceAccountException(storedProcedureReport.callerReport()); } return storedProcedureReport.callerReport(); } if (IS_PARTNER_CONNECT) { execStatement('ALTER WAREHOUSE PC_ALTR_WH SET AUTO_SUSPEND = 30'); permissionRole('"PC_ALTR_ROLE"'); } else { permissionUser('"ALTR_SERVICE_ROLE"', '"ALTR_SERVICE_USER"', '"ALTR_SERVICE_WH"'); } report = storedProcedureReport.callerReport(); if (storedProcedureReport.isSuccess) { execStatement("ALTER PROCEDURE IF EXISTS {{.Database}}.{{.Schema}}.SETUP_ALTR_SERVICE_ACCOUNT(BOOLEAN) SET COMMENT = '" + new Date().toUTCString() + "'"); } else if (RUN_AS_ALTR) { throw new AltrServiceAccountException(report) } return report; $$;
Execute the following command to run the stored procedure. The stored procedure may take several minutes to run, especially for Snowflake accounts with many databases, schemas and tables.
Note
If using PC_ALTR_USER, set the parameter to TRUE. If using ALTR_SERVICE_USER, set the parameter to FALSE.
CALL SETUP_ALTR_SERVICE_ACCOUNT(FALSE);
Note
ALTR recommends assigning a network policy to the service user to further secure the user and to prevent unauthorized access. Learn more.