0.1.7 • Published 2 months ago

plainid-pdp-sdk v0.1.7

Weekly downloads
-
License
-
Repository
-
Last release
2 months ago

PlainID Policy Decision Point (PDP) SDK

The PlainID PDP SDK is a library that can be used to easily integrate the PlainID's Externalized Authorization into your applications.

The SDK utilizes the following functionality of the PlainID PDP:

  • Access Decision - evaluate PERMIT/DENY access requests
  • Resolution Decision - evaluate data access requests

Installation

npm i plainid-pdp-sdk

Access Enforcement (API)

const express = require('express')
const pdp = require('plainid-pdp-sdk');

const pdpClient = new pdp.v5.Client({ 
    url: 'https://demo.plainid.cloud', 
    clientId: 'P6POYCA8QY5', clientSecret: 'XRp8OXVugWlnDZjP8', authorizerId: 'APA9G5HV7WXN9C' 
})

const app = express()
app.use(async (req, res, next) => {
    const payload = {
        method: req.method,
        headers: req.getHeaders(),
        url: req.url,
        body: req.body,
        meta: { runtimeFineTune: { includeIdentity: 'true' } }
    }

    const response = await pdpClient.accessDecision().evaluate(payload)
    if (!response.access) {
        throw new Error('Forbidden')
    }
})

Data Enforcement (SQL)

const knex = require('knex');
const pdp = require('plainid-pdp-sdk');
const pdpClient = new pdp.v3.Client({
    url: 'https://demo.plainid.cloud',
    clientId: 'P6POYCA8QY5', clientSecret: 'XRp8OXVugWlnDZjP8', authorizerId: 'APA9G5HV7WXN9C'
})

const tableName = 'members'
const Members = knex(tableName) // initiate query builder

const payload = {
    entityId: 'john.doe@gmail.com',
    entityTypeId: 'User',
    includeAssetAttributes: true,
}
const response = await pdpClient.resolutionDecision().evaluate(payload)
const tableRestrictions = response.resolution.getResourceRestrictions(tableName)!

// Column-level enforcement
const selectColumns = []
tableRestrictions.getAssets().forEach(asset => {
    selectColumns.push(asset.isMasked() ? `${asset.getMaskAs()} as ${asset.getName()}` : asset.getName())
})
Members.select(selectColumns)

// Row-level enforcement
const whereClause = tableRestrictions.getConditionSql() // ( ( ('ID'  > 1 ) )  AND ( ('Country'  LIKE '%Morocco%' ) ) )
Members.where(knex.raw(whereClause))

const resultSet = await Members

Advanced Usage

The PlainID PDP SDK provides two actual versions of policy resolution - v3 and v5.

  • for Access Enforcement (accessDecision) it is v5.
  • for Data Enforcement (resolutionDecision) it is recommended to use v3.

Authentication

v5

Using clientId and clientSecret:

import {Client} from 'plainid-pdp-sdk/v5'

const options = <ClientConfigInterface>{
    url: 'https://demo.plainid.cloud',
    clientId: 'P6POYCA8QY5', clientSecret: 'XRp8OXVugWlnDZjP8', authorizerId: 'APA9G5HV7WXN9C'
} 
const client: ClientInterface = new Client(options)

v3

Using clientId and clientSecret

import {Client} from 'plainid-pdp-sdk/v3'

const config = <ClientConfigInterface>{
    url: 'https://demo.plainid.cloud',
    clientId: 'P6POYCA8QY5', clientSecret: 'XRp8OXVugWlnDZjP8', authorizerId: 'APA9G5HV7WXN9C'
} 
const client = new Client(config)

Using JWT:

If included in the JWT payload, the values for clientSecret and entityId are optional for this scenario and will be used from there.

import {Client} from 'plainid-pdp-sdk/v3'

const config = <ClientConfigInterface>{
    url: 'https://demo.plainid.cloud',
    clientId: 'P6POYCA8QY5', authorizerId: 'APA9G5HV7WXN9C'
} 
const client = new Client(config)

const options = <AccessDecisionOptionsInterface>{
    headers: {
        'Authorization': 'Bearer J.W.T'
    }
}

const response: ResolutionResponse = await client.resolutionDecision(options).evaluate(payload)

Interfaces

interface ClientConfigInterface {
    url: string             // PDP url
    clientId: string        // client id of needed scope
    clientSecret?: string   // client secret of needed scope
    authorizerId: string    // authorizer id
}

interface ClientInterface {
    getConfig(): ClientConfigInterface;
    resolutionDecision(options: ResolutionDecisionOptionsInterface): ResolutionDecisionInterface;
    accessDecision(options: AccessDecisionOptionsInterface): AccessDecisionInterface;
}

Access Decision Request (v5)

import {Client} from 'plainid-pdp-sdk/v5'
const payload = <AccessDecisionPayloadInterface>{
    url: 'https://example.com/full/path?param1=value11&param1=value12&param2=value2#some_chapter',
    method: 'POST',                             
    headers: { 'header-name': 'header value' },
    body: { some: 'data' },
    meta: { runtimeFineTune: { includeIdentity: 'true' } }
}

const config = <AccessDecisionOptionsInterface>{
    maxAttempts: 2,
    delay: 100,
    headers: {'some-header': 'some value'},
}
const accessDecision: AccessDecisionInterface = client.accessDecision(config)
const response: AccessDecisionResponseInterface = await accessDecision.evaluate(payload)

Interfaces

interface AccessDecisionPayloadInterface {
    url: string                                 // HTTP authorizing request url (required)
    method?: string                             // HTTP request method: POST, GET, PUT, PATCH, DELETE, ...
    headers: Record<string, string | string[]>  // HTTP request headers
    body?: any                                  // HTTP request body
    meta?: Record<string, any>                  // Additional parameters
    // Inside `meta` can be used the attribute `runtimeFineTune: Record<string, string>` - Any request attribute understandable by PDP (Runtime)
}

interface AccessDecisionOptionsInterface {
    maxAttempts?: number                        // Maximum number of attempts to retry the request. Default: `1`.
    delay?: number                              // Delay between retries in milliseconds. Default: `0`.
    schema: 'http' | 'https'                    // PDP request schema. Default: `https`.
    headers?: Record<string, string>            // Additional headers for PDP commucication.
}

interface AccessDecisionResponseInterface {
    statusCode: number                          // http response status code
    headers: Record<string, string>             // http response headers
    body: string                                // http raw response body
    access: boolean | null                      // acess decision
}

interface AccessDecisionInterface {
    getClient(): ClientInterface;
    getOptions(): AccessDecisionOptionsInterface;
    evaluate(payload: AccessDecisionPayloadInterface): Promise<AccessDecisionResponseInterface>;
}

Resolution Decision Request (v3)

import {Client, AccessDecisionPayloadInterface, ResolutionResponse} from 'plainid-pdp-sdk/v3'
const payload = <AccessDecisionPayloadInterface> {
    entityId: 'john.doe@gmail.com',
    entityTypeId: 'User',
    remoteIp: '1.2.2.1',
    includeAssetAttributes: true,
}
const options = <AccessDecisionOptionsInterface>{
    maxAttempts: 3,
    schema: 'http',
    delay: 100,
    headers: {
        'x-custom-header': 'custom value'
    },
    resolutionConfig: {
        resolutionAttributeProjectId: 'projectid',
        resolutionAttributeDataset: 'dataset',
        resolutionAttributeTable: 'table',
        resolutionAttributeColumn: 'column',
    }
}

const response: ResolutionResponse = await client.resolutionDecision(options).evaluate(payload)

Interfaces

interface ResolutionDecisionOptionsInterface {
    maxAttempts?: number                            // Maximum number of attempts to retry the request. Default: `1`.
    schema: 'http' | 'https'                        // PDP request schema. Default: `https`.
    delay?: number                                  // Delay between retries in milliseconds. Default: `0`.
    headers?: Record<string, string>                // Additional headers for PDP commucication.
    resolutionConfig?: ResolutionConfigInterface    // Resolution config to be used with parsing the PDP response
}

interface ResolutionConfigInterface {
    resolutionAttributeProjectId?: string
    resolutionAttributeDataset?: string
    resolutionAttributeTable?: string
    resolutionAttributeColumn?: string
    resolutionAttributeMaskAs?: string
    resolutionAttributeMaskFilter?: string
    resolutionMaskingResourceTypeName?: string
}

interface ResolutionDecisionResponseInterface {
    statusCode: string                              // http status code of the response
    headers: Map<string, string>                    // http headers of the response
    body: string                                    // http raw body of the response
    resolution: Resolution | null                   // parsed PDP restrictions and helpers
}

interface Resolution {
    getConfig(): Record<string, any>
    getAllRestrictions(): Map<string, Restrictions>
    getResourceRestrictions(fullResourceName: string): Restrictions
    getAllTables(): Set<string>
    isAssetMasked(fullResourceName: string, assetName: string): boolean
    isAssetAllowed(fullResourceName: string, assetName: string): boolean
    existAsset(fullResourceName: string, assetName: string): boolean
    existAllowedAssets(fullResourceName: string): boolean
}

interface Restrictions {
    FULL_CONDITION_FIELD_NAME: boolean
    FIELD_APOSTROPHES: [string, string]
    isEmpty(): boolean
    mergeWith(restrictions: Restrictions | undefined): Restrictions
    addAllowedConditionSql(condition: string): Restrictions
    addDeniedConditionSql(condition: string): Restrictions
    getConditionSql(): string   // (('Country' LIKE '%United%') AND ('balance' < 1000))
    getConditionFields(): Set<RestrictionsConditionField>
    getIsAccessEmpty(): boolean
    setIsAccessEmpty(isAccessEmpty: boolean): this
    addAsset(asset: RestrictionsAsset): Restrictions
    getAssets(): Map<String, RestrictionsAsset>
    getAsset(assetName: string): Optional<RestrictionsAsset>
}

interface RestrictionsConditionField {
    name: string                // field name, ex: "balance"
    type: string                // field type, ex: "NUMERIC", "STRING"
}

interface RestrictionsAsset {
    fullResourceName: string    // full resource name, ex: "dbName.tableName"
    name: string                // column name, ex: "balance"
    maskAs: string              // mask for the column, ex: "<censored>"
}

All conditions in one SQL where clause

Restrictions.FULL_CONDITION_FIELD_NAME = true
Restrictions.FIELD_APOSTROPHES = ['', '']
const response = await client.resolutionDecision(options).evaluate(payload)
const allRestrictions = response.resolution.getAllRestrictions()
const whereClause = Array.from(allRestrictions.values()).map(restriction => restriction.getConditionSql()).join(' AND ')
console.log(whereClause) // ((mydb.members.Country LIKE '%United%') AND (mydb.accounts.balance < 1000))

Error handling

Access Decision response (v5)

try {
    const response = await client.resolutionDecision(options).evaluate(payload)
} catch (err) {
    console.error(err.message)  // Example: "Error: getaddrinfo ENOTFOUND api.example.com"
}

Interfaces

interface SdkError {
    id: string                      // unique id of the error
    code: string                    // error code
    name: string                    // code name
    message: string                 // message
    args?: Record<string, string>   // additional arguments
}

Resolution Decision response (v3)

try {
    const response = await client.resolutionDecision(options).evaluate(payload)
} catch (err) {
    console.error(err.message)  // Example: "Error: getaddrinfo ENOTFOUND api.example.com"
}

Troubleshooting

Error: self-signed certificate in the certificate chain

You can fix this issue using NODE_TLS_REJECT_UNAUTHORIZED=0 in the terminal, or insert the following line within the JS file:

process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;

This is a flag which helps avoid a rejection of an unauthorized (self-signed) certification. It is not recommended to use in a production environment.

0.1.7

2 months ago

0.1.6

7 months ago

0.1.5

7 months ago

0.1.4

7 months ago

0.0.1-rc.4

7 months ago

0.1.3

7 months ago

0.0.1-rc.3

7 months ago

0.0.1-rc.1

7 months ago

0.0.1-rc.0

7 months ago

0.1.2

7 months ago

0.1.2-rc.0

7 months ago

0.1.1

7 months ago