How to hide Data Extensions in Contact Builder and Email Studio using server-side Javascript

How to hide Data Extensions in Contact Builder and Email Studio using server-side Javascript

This article explains how to hide Data Extensions from the Marketing Cloud user interface (Contact Builder and Email Studio).

Use case

Storing sensitive data in Data Extensions (such as passwords, hashes or API endpoint) is always a risk.

As a matter of fact, Data Extensions’ data can be easily accessed through the Content Builder and Email Studio applications by whomever has the right permission. In this case, some sensitive data can be stolen or worse – deleted, on purpose or by accident.

Unfortunately, there is no feature in Marketing Cloud today that allows for selective Data Extension restrictions.

So, what are we waiting for? Let’s build one ourselves!

How does it work

In order to hide a Data Extension from the user interface, we need to make it folderless. In other words, we need to remove the association between the folder and the Data Extension by deleting the folder, but not the DE.

Obviously, the application won’t let us do that, by prompting us with a warning that a folder with contents cannot be deleted.

Luckily for us, there is no such limitation when it comes to server-side JavaScript.

Hide the Data Extension

In this exemple, we are using the WSProxy library, for its speed and ease of use.

<script runat="server">
    Platform.Load("core", "1.1");
    var api = new Script.Util.WSProxy();
	/* YOUR CODE GOES HERE */
</script>

Retrieve the Data Extension

In order to retrieve the correct Data Extension, we need to use a unique identifier, which happens to be Customer Key.

Using the Customer Key we can easily fetch the CategoryID of the Data Extension, which corresponds to the ID of the Folder object.

var deRequest = api.retrieve("DataExtension", ["Name","CategoryID"], {
	Property: "DataExtension.CustomerKey",
	SimpleOperator: "equals",
	Value: "XXXX-XXXXX-XXXX-XXXXXX"
});
var catId = deRequest.Results[0].CategoryID;

Create a temporary sub-folder

Using the CategoryID we just retrieved, we can create a sub-folder called TEMP_FOLDER and store its ID and ObjectID for later use.

var createNewFolderRequest = api.createItem("DataFolder", {
	"Name": "TEMP_FOLDER",
	"Description": "Temporary API Created Folder",
	"ParentFolder": {
		ID : catId,
		IDSpecified: true
	},
	"ContentType": "dataextension"
}); 
var newObjectID = createNewFolderRequest.Results[0].NewObjectID;
var newCategoryID = createNewFolderRequest.Results[0].NewID;

Move and update the Data Extension

Moving a Data Extension to a new folder is a piece of cake: update the Data Extension object with a new CategoryID.

Note that we also store the original CategoryID in the Description property.

var moveDeRequest = api.updateItem("DataExtension", {
	"CustomerKey": customerKey,
	"Description": catId,
	"CategoryID": newCategoryID
});

Delete the temporary folder

In order to delete anything in Marketing Cloud, we need to provide the ObjectID property. In this case, we are deleting the new folder we created.

var deleteFolderRequest = api.deleteItem(
	"DataFolder", 
	{ "ObjectID": newObjectID }
);

And we are done! We have successfully created a temporary folder, stored the original folder ID in the description of our Data Extension and then deleted the folder without erasing the DE.

Restore the Data Extension to its original folder

Now that we have hidden our Data Extension, it’s important to be able to restore it to the original folder.

Retrieve the original folder ID

As previously mentioned, the original folder ID is stored in the Description property of our Data Extension.

var deRequest = api.retrieve("DataExtension", ["Name", "Description"], {
	Property: "DataExtension.CustomerKey",
	SimpleOperator: "equals",
	Value: "XXXX-XXXXX-XXXX-XXXXXX"
});
var catId = (deRequest.Results[0].Description.length > 0) ? Number(deRequest.Results[0].Description) : "";

Move to the original folder

As a last step, we need to update our Data Extension with the original folder ID (CategoryID property).

var moveDeRequest = api.updateItem("DataExtension", {
	"CustomerKey": "XXXX-XXXXX-XXXX-XXXXXX",
	"Description": " ",
	"CategoryID": catId
});

And voilà! The Data Extension is again visible in the Marketing Cloud UI.

Building an App

Because why not an app! Making your colleagues believe their precious Data Extension has been permanently deleted is the best prank a Marketing Cloud developer can pull!

Cloud page

Vue and Bootstrap are kings when it comes to building a simple user interface within minutes. Google Fonts and FontAwesome icons are always welcome as well to beautify the content.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
    <title>Hide Data Extension</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" rel="stylesheet" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
    <script src="https://kit.fontawesome.com/680fcb47ef.js" crossorigin="anonymous"></script>   
    <link href="https://fonts.googleapis.com/css2?family=DM+Mono&display=swap" rel="stylesheet">
    <style>
        h2 {
            font-family: 'DM Mono', monospace;
        }
    </style>
</head>
<body>
<div id="app" class="container">
    <div class="col-md-7 mx-auto my-5">
        <template>
            <h2><i class="fas fa-eye-slash"></i> Hide Data Extension</h2>
            <form class="needs-validation my-4" ref="form" action="{{ URL TO YOUR FORM HANDLER }}" method="post">
                <div class="input-group input-group-lg">
                    <input 
                        class="form-control"
                        v-model="form.customerKey"
                        type="text"
                        minlength="3"
                        maxlength="254"
                        placeholder="Customer Key..."
                        required
                    >
                    <div class="input-group-append">
                        <button 
                            @click.prevent="takeAction()"
                            type="button" 
                            class="btn btn-primary"
                            :disabled="(status == 101)"
                        >
                        {{ button }}
                        </button>
                        <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <div class="dropdown-menu">
                            <a class="dropdown-item" href="#" @click.prevent="action = 'hide';">Hide</a>
                            <a class="dropdown-item" href="#" @click.prevent="action = 'show';">Restore</a>
                        </div>
                    </div>
                </div>
            </form>
        </template>
        <template v-if="status == 100"></template>
        <template v-if="status == 101">
            <div class="text-center text-primary">
                <div class="spinner-border" role="status">
                    <span class="sr-only"></span>
                </div>
            </div>
        </template>
        <template v-if="status == 200 && action.length > 0">
            <div class="row">
                <div class="col-12">
                    <div class="alert-success p-4 rounded"> 
                        {{ message }}
                    </div>
                </div>
            </div>
        </template>
        <template v-if="status == 404">
            <div class="text-left">
                <h3>Sorry but nothing was found.</h3>
            </div>
        </template>
        <template v-if="status == 500">
            <div class="text-left">
                <div class="alert-danger p-4 rounded"> 
                    Something went wrong: {{ message }}
                </div>
            </div>
        </template>
    </div>
</div>
<script src="{{ URL TO YOUR JS FILE }}"></script>
</body>
</html>

App.js

The App itself is quiet simple as well. We only need to send 2 parameters to the Form Handler page: Customer Key and Action. The latter corresponds to the action we need to perform: hide or show the Data Extension.

The request to the Form Handler is performed though AJAX using the Axios library and the response will be an object with Status and Message keys, used to display the error or success of the operation.

new Vue({
	el: '#app',
    data: {
        status: 100,
        form: {
            customerKey: '',
            action: ''
        },
        button: 'Choose an action',
        action: '',
        endpoint: '',
        message: ''
    },
    watch: {
        action: function () {
            this.$refs.form.classList.remove('was-validated');
            switch (this.action) {
                case 'hide':
                    this.button = 'Hide';
                    break;
                case 'show':
                    this.button = 'Restore'
                    break;    
                default:
                    this.button = 'Choose an action'
                    break;
            }
            this.status = 100;
        }
    },
    mounted: function() {
        this.endpoint = this.$refs.form.getAttribute('action');
    },
    methods: {

        sendFormData: function() {
            this.status = 101;
            this.form.action = this.action;
            var $this = this;
            axios({
                method: 'POST',
                url: this.endpoint,
                data: this.form,
                validateStatus: function() { return true }
            }).then(function(result) {
                $this.status = result.data.Status;
                $this.message = result.data.Message;
            }).catch(function(error) {
                console.error(error);
            });
        },

        validateForm: function() {
            if (this.$refs.form.checkValidity() !== false) {
                this.sendFormData();
            }
            this.$refs.form.classList.add('was-validated');
        },

        takeAction: function() {
            if(this.action == 'hide' || this.action == 'show') {
                this.validateForm();
            } else {
                return false;
            }
        }
    }
})

Form Handler

Depending on the value of the Action parameter, the Form Handler will show or hide the Data Extension.

Please note that in order for the app to be easily debugged and monitored, we need to create some exception handling.

Better yet, why not include the response from each WSProxy request in the output object?

<script runat="server">
Platform.Load("core", "1.1");

var api = new Script.Util.WSProxy();
var postData = Platform.Request.GetPostData();
var form = Platform.Function.ParseJSON(postData);
var customerKey = form.customerKey;
var hide = (form.action == 'hide');

try {
    if(hide) {

        /// GET DATA EXTENSION FOLDER
        var deRequest = api.retrieve("DataExtension", ["Name","CategoryID"], {
            Property: "DataExtension.CustomerKey",
            SimpleOperator: "equals",
            Value: customerKey
        });

        throwOnError(deRequest, "Could not find the Data Extension with Customer Key \"" + customerKey + "\"");

        var catId = deRequest.Results[0].CategoryID;
        var deName = deRequest.Results[0].Name;

        /// CREATE CHILD TEMP FOLDER
        var createNewFolderRequest = api.createItem("DataFolder", {
            "Name": "TEMP_FOLDER",
            "Description": "Temporary API Created Folder",
            "ParentFolder": {
                ID : catId,
                IDSpecified: true
            },
            "ContentType": "dataextension"
        }); 
        throwOnError(createNewFolderRequest, "Could not create the TEMP folder.");

        var newObjectID = createNewFolderRequest.Results[0].NewObjectID;
        var newCategoryID = createNewFolderRequest.Results[0].NewID;

        /// MOVE DATA EXTENSION TO TEMP FOLDER & PUT CATEGORY ID IN THE DESCRIPTION
        var moveDeRequest = api.updateItem("DataExtension", {
            "CustomerKey": customerKey,
            "Description": catId,
            "CategoryID": newCategoryID
        });

        throwOnError(moveDeRequest, "Could not move the Data Extension to the TEMP folder.");

        /// DELETE TEMP FOLDER
        var deleteFolderRequest = api.deleteItem("DataFolder", { "ObjectID": newObjectID });

        throwOnError(deleteFolderRequest, "Could not delete the TEMP folder.");

        var response = { 
            Status: 200, 
            Message: "The Data Extension \"" + deName + "\" has been hidden.",
            Details: {
                DeRequest: deRequest,
                CreateNewFolderRequest: createNewFolderRequest,
                MoveDERequest: moveDeRequest,
                DeleteFolderRequest: deleteFolderRequest
            }
        }

        Write(Stringify(response));
    } else {

        /// RETRIEVE ORIGIN FOLDER
        var deRequest = api.retrieve("DataExtension", ["Name", "Description"], {
            Property: "DataExtension.CustomerKey",
            SimpleOperator: "equals",
            Value: customerKey
        });

        throwOnError(deRequest, "Could not find the Data Extension with Customer Key \"" + customerKey + "\"");

        var catId = (deRequest.Results[0].Description.length > 0) ? Number(deRequest.Results[0].Description) : "";
        var deName = deRequest.Results[0].Name;

        /// VERIFY FOLDER EXISTS
        var folderExistsRequest = api.retrieve("DataFolder", ["Name","ID"], {
            Property: "ID",
            SimpleOperator: "equals",
            Value: catId
        });

        if(folderExistsRequest.Status != "OK" || folderExistsRequest.Results.length < 1) {

            /// RETRIEVE ROOT FOLDER ID
            var folderRequest = api.retrieve("DataFolder", ["ID"], {
                Property: "Name",
                SimpleOperator: "equals",
                Value: "Data Extensions"
            });

            throwOnError(folderRequest, "Could not retrieve the root folder ID.");

            rootCategoryID = folderRequest.Results[0].ID;

            /// MOVE DATA EXTENSION TO ROOT FOLDER
            var moveDeRequest = api.updateItem("DataExtension", {
                "CustomerKey": customerKey,
                "Description": " ",
                "CategoryID": rootCategoryID
            });

            throwOnError(moveDeRequest, "Could not move the Data Extension to the root folder.");

            var response = { 
                Status: 200, 
                Message: "The Data Extension \"" + deName + "\" has been restored to the root folder.",
                Details: {
                    DeRequest: deRequest,
                    FolderExistsRequest: folderExistsRequest,
                    FolderRequest: folderRequest,
                    MoveDERequest: moveDeRequest
                }
            }

            Write(Stringify(response));

        } else {    

            folderName = folderExistsRequest.Results[0].Name;

            /// MOVE DATA EXTENSION TO ORIGIN FOLDER
            var moveDeRequest = api.updateItem("DataExtension", {
                "CustomerKey": customerKey,
                "Description": " ",
                "CategoryID": catId
            });
            throwOnError(moveDeRequest, "Could not move the Data Extension to \"" + folderName + "\" folder.");
            var response = { 
                Status: 200, 
                Message: "The Data Extension \"" + deName + "\" has been restored to \"" + folderName + "\" folder.", 
                Details: {
                    DeRequest: deRequest,
                    FolderExistsRequest: folderExistsRequest,
                    MoveDERequest: moveDeRequest
                }
            }
            Write(Stringify(response));
        }
    }
} catch (error) {
    Write(Stringify({ Status: 500, Message: error }));
}
function throwOnError(req, message) {
    if(req.Status != "OK" || req.Results.length < 1) {
        throw message;
    }
}
</script>

Conclusion

I understand that this method is basically exploiting one of the Marketing Cloud’s shortcomings, but as long as it works, it’s good enough for me.

I imagine that some day, some random Marketing Cloud developer will add an exception in the server-side JavaScript when someone tries to delete a folder that contains Data Extensions. Or even better, imagine someone gives them the idea to create password-protected Data Extensions.

Who knows what the future holds, but for now, this method is the best we got.

Pay me a coffee

Want to say thanks? Pay me a coffee! Remember, I turn coffee into code.

Have I missed anything?

Please poke me with a sharp comment below or use the contact form.

Leave a Reply

Your email address will not be published. Required fields are marked *

Up Next:

How to build a Search and Replace app in Salesforce Marketing Cloud with Vue, SSJS and REST API

How to build a Search and Replace app in Salesforce Marketing Cloud with Vue, SSJS and REST API