auth/AuthUnit.js

/**
 * @module flitter-auth/AuthUnit
 */

const Unit = require('libflitter/Unit')
const ncp = require('ncp')
const path = require('path')
const oauth2 = require('node-oauth2-server')
const flapper = require('flitter-flap/FlapHelper')(false)

const FlitterProvider = require('./flitter/FlitterProvider')
const LdapProvider = require('./ldap/LdapProvider')
const Oauth2Provider = require('./oauth2/Oauth2Provider')

const Oauth2BearerToken = require('./model/Oauth2BearerToken')
const Oauth2Client = require('./model/Oauth2Client')
const Oauth2AuthorizationTicket = require('./model/Oauth2AuthorizationTicket')

const SecurityContext = require('./SecurityContext')

/**
 * Registers functionality provided by flitter-auth.
 * @extends module:libflitter/Unit~Unit
 */
class AuthUnit extends Unit {
    /**
     * Defines the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'configs', 'canon', 'models', 'output']
    }

    /**
     * Get the name of the service this unit represents - 'auth'
     * @returns {string} - 'auth'
     */
    static get name() {
        return 'auth'
    }

    /**
     * Instantiate the unit.
     */
    constructor() {
        super()

        /**
         * Mapping of provider names to instances of the {module:flitter-auth/Provider~Provider} classes.
         * @type {object}
         */
        this.providers = {}

        /**
         * Mapping of provider class names to static provider classes.
         * @type {object}
         */
        this.provider_classes = {
            FlitterProvider,
            LdapProvider,
            Oauth2Provider,
        }
    }

    /**
     * Initialize the unit:
     *  - Registers the auth and authProvider canonical resources
     *  - For all configured auth sources, create a new instance of the applicable provider
     *    and register it with this service.
     *
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @returns {Promise<void>}
     */
    async go(app){
        // Register the providers resolver as a canonical resource
        this.canon.register_resource('auth', this.get_provider_instance.bind(this))
        this.canon.register_resource('authProvider', this.resolve_provider.bind(this))

        this.app.di().inject(SecurityContext)

        // Initialize Oauth2 Support
        // await this.init_oauth()

        if ( this.configs.get('auth') ) {
            const sources = this.configs.get('auth.sources')
            for ( const name in sources ) {
                if ( !sources.hasOwnProperty(name) ) continue

                const source_config = sources[name]
                if ( source_config.enable ) {
                    source_config.name = name
                    const ProviderClass = this.resolve_provider(source_config.type)
                    if ( !ProviderClass ) this.output.error(`Unknown auth provider type: ${source_config.type}`)
                    else this.output.info(`Registered auth provider: ${name} (${source_config.type})`)


                    this.providers[name] = new ProviderClass(app, source_config)
                }
            }
        }

        await this.init_oauth()
    }

    /**
     * Get the auth source instance with the specified name.
     * @param {string} name
     * @returns {module:flitter-auth/Provider~Provider}
     */
    get_provider_instance(name) {
        return this.providers[name]
    }

    /**
     * Get the auth provider class definition with the specified name.
     * @param {string} name
     * @returns {module:flitter-auth/Provider~Provider} - the static CLASS reference
     */
    resolve_provider(name) {
        const value = this.provider_classes[name]

        const di = this.app.di()
        if ( di.__is_injectable(value) ) return di.inject(value)

        return value
    }

    /**
     * Deploy the resources provided by flitter-auth. That is, the controllers/views/routes/models/middleware/etc.
     * @returns {Promise<void>}
     */
    async deploy() {
        const package_dir = __dirname
        // const base_dir = path.dirname(require.main.filename)
        this.output.info("Auth deploy from: " + package_dir)
        this.output.info("To: " + path.dirname(require.main.filename))

        function do_copy(from, to) {
            return new Promise(
                (resolve, reject) => {
                    ncp(from, to, (error) => {
                        if (error) reject(error)

                        resolve()
                    })
                }
            )
        }
        
        
        // controllers
        this.output.message('Deploying controllers...')
        await do_copy(path.resolve(package_dir + '/deploy/controllers'), this.app.directories.controllers)

        // models
        this.output.message('Deploying models...')
        await do_copy(path.resolve(package_dir + '/deploy/models'), this.app.directories.models)

        // routing - middleware
        this.output.message('Deploying middleware...')
        await do_copy(path.resolve(package_dir + "/deploy/routing/middleware"), path.resolve(this.app.directories.middlewares, 'auth'))

        // routing - routers
        this.output.message('Deploying routers...')
        await do_copy(path.resolve(package_dir + "/deploy/routing/routers"), this.app.directories.routers)

        // views
        this.output.message('Deploying views...')
        await do_copy(path.resolve(package_dir + "/deploy/views/auth"), path.resolve(this.app.directories.views, 'auth'))
        
        // config
        this.output.message('Deploying config...')
        await do_copy(path.resolve(package_dir + "/deploy/config"), this.app.directories.configs)
        
        // assets
        this.output.message('Deploying assets...')
        await do_copy(path.resolve(package_dir + "/deploy/assets"), path.resolve(this.app.directories.assets, 'auth'))
        
        // add global middleware
        this.output.message('Adding global middleware...')
        await flapper.insert_line(this.app.directories.global_middleware, /\s*=\s*\[/, '    "auth:Utility",')

    }

    /**
     * Get the fully-qualified path to the migrations provided by this unit.
     * @returns {string}
     */
    migrations(){
        return path.resolve(__dirname, 'migrations')
    }

    /**
     * Clean up the resources managed by this unit. Calls the 'cleanup' method
     * on all registered providers in this service.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @returns {Promise<void>}
     */
    async cleanup(app){
        for ( let key in this.providers ){
            if ( !this.providers.hasOwnProperty(key) ) continue
            await this.providers[key].cleanup(app)
        }
    }

    /**
     * Get the provider configured with the specified name. If
     * no name is provided, the default provider will be returned.
     * @param {string} [name]
     * @returns {module:flitter-auth/Provider~Provider}
     */
    get_provider(name){
        if ( !name ) {
            name = this.configs.get('auth.default_provider')
        }

        return this.providers[name]
    }

    /**
     * Initializes the OAuth2 server's resources.
     * @returns {Promise<void>}
     */
    async init_oauth() {
        let config = this.configs.get('auth')
        if ( !config ) return
        else config = config.servers.oauth2

        this.models.external_model(this, 'Oauth2BearerToken', Oauth2BearerToken)
        this.models.external_model(this, 'Oauth2Client', Oauth2Client)
        this.models.external_model(this, 'Oauth2AuthorizationTicket', Oauth2AuthorizationTicket)

        this.app.express.oauth2 = oauth2({
            model: this.models.get('auth::Oauth2BearerToken'),
            grants: config.grants,
            debug: true,
        })

        this.app.express.use(this.app.express.oauth2.errorHandler())
    }
}

module.exports = exports = AuthUnit