├── .npmignore ├── test ├── test.conf.js ├── functional │ ├── .eslintrc │ ├── fixtures │ │ ├── .editorconfig │ │ ├── binary │ │ ├── package.ts │ │ └── tags.json │ ├── lib │ │ ├── setup.ts │ │ ├── mock_gitlab_server.ts │ │ └── environment.ts │ ├── teardown.js │ ├── test-environment.js │ ├── pre-setup.js │ ├── basic │ │ ├── ping.ts │ │ ├── whoami.ts │ │ └── index.ts │ ├── index.spec.ts │ ├── store │ │ └── config-1.yaml │ ├── access │ │ └── index.ts │ ├── auth │ │ └── index.ts │ ├── config.functional.ts │ └── publish │ │ └── index.ts ├── unit │ ├── partials │ │ ├── logger.ts │ │ └── config │ │ │ └── index.ts │ ├── authcache.spec.ts │ └── gitlab.spec.ts ├── jest.config.functional.js ├── lib │ ├── utils.ts │ ├── verdaccio-server.ts │ ├── constants.ts │ ├── server_process.ts │ ├── request.ts │ └── server.ts ├── jest.config.unit.js ├── __mocks__ │ └── gitlab.js └── types │ └── index.ts ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── src ├── verdaccio.ts ├── index.ts ├── authcache.ts └── gitlab.ts ├── bin └── verdaccio ├── .dockerignore ├── .editorconfig ├── .prettierrc ├── .travis.yml ├── tsconfig.json ├── conf └── docker.yaml ├── docker-compose.yml ├── LICENSE.txt ├── Dockerfile ├── package.json ├── README.md └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !bin/* 3 | !build/*.js 4 | -------------------------------------------------------------------------------- /test/test.conf.js: -------------------------------------------------------------------------------- 1 | export const DOMAIN_SERVERS = '0.0.0.0'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false 3 | } 4 | -------------------------------------------------------------------------------- /test/functional/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "@typescript-eslint/no-unused-vars": 0 5 | } 6 | } -------------------------------------------------------------------------------- /test/functional/fixtures/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = ignore 6 | 7 | -------------------------------------------------------------------------------- /test/functional/fixtures/binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferoverflow/verdaccio-gitlab/HEAD/test/functional/fixtures/binary -------------------------------------------------------------------------------- /test/functional/lib/setup.ts: -------------------------------------------------------------------------------- 1 | module.exports = async function() { 2 | // here we should create dynamically config files 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | build/ 4 | coverage/ 5 | tests-report/ 6 | test-storage* 7 | yarn-error.log 8 | .idea/* 9 | -------------------------------------------------------------------------------- /src/verdaccio.ts: -------------------------------------------------------------------------------- 1 | import globalTunnel from 'global-tunnel-ng'; 2 | globalTunnel.initialize(); 3 | require('verdaccio/build/lib/cli'); 4 | -------------------------------------------------------------------------------- /test/functional/teardown.js: -------------------------------------------------------------------------------- 1 | import { blue } from 'kleur'; 2 | 3 | module.exports = async () => { 4 | console.log(blue('teardown: all servers closed')); 5 | }; 6 | -------------------------------------------------------------------------------- /bin/verdaccio: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.env.NODE_PATH = __dirname + '/../..'; 4 | require("module").Module._initPaths(); 5 | 6 | require('../build/verdaccio.js'); 7 | -------------------------------------------------------------------------------- /test/functional/test-environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | require('@babel/register')({ 4 | extensions: ['.ts', '.js'], 5 | }); 6 | module.exports = require('./lib/environment'); 7 | -------------------------------------------------------------------------------- /test/functional/pre-setup.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | extensions: ['.ts', '.js'] 3 | }); 4 | 5 | import { blue } from 'kleur'; 6 | 7 | module.exports = async () => { 8 | console.log(blue('setup: starting servers')); 9 | require('./lib/setup'); 10 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | flow-typed/ 4 | 5 | # dot files by default excluded 6 | .* 7 | # dot file exceptions 8 | !.babelrc 9 | !.editorconfig 10 | !.eslintignore 11 | !.eslintrc* 12 | !.flowconfig 13 | 14 | Dockerfile* 15 | docker-compose*.yml 16 | -------------------------------------------------------------------------------- /test/functional/basic/ping.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | module.exports = server => { 4 | test('ping', () => { 5 | return server.ping().then(data => { 6 | // it's always an empty object 7 | expect(_.isObject(data)).toBeDefined(); 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/functional/basic/whoami.ts: -------------------------------------------------------------------------------- 1 | import { CREDENTIALS } from '../config.functional'; 2 | 3 | module.exports = server => { 4 | test('whoami', () => { 5 | return server.whoami().then(username => { 6 | expect(username).toBe(CREDENTIALS.user); 7 | }); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 160, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "requirePragma": true, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": true, 9 | "trailingComma": "es5", 10 | "semi": true, 11 | "parser": "typescript" 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | services: 3 | - docker 4 | sudo: false 5 | node_js: 6 | - 'lts/dubnium' 7 | - 'lts/*' 8 | script: 9 | - yarn install 10 | - commitlint-travis 11 | - yarn license 12 | - yarn lint 13 | - yarn code:build 14 | - yarn test:all 15 | - yarn build:docker 16 | -------------------------------------------------------------------------------- /test/unit/partials/logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@verdaccio/types'; 2 | 3 | const logger: Logger = { 4 | error: jest.fn(), 5 | info: jest.fn(), 6 | debug: jest.fn(), 7 | child: jest.fn(), 8 | warn: jest.fn(), 9 | http: jest.fn(), 10 | trace: jest.fn() 11 | }; 12 | 13 | export default logger; 14 | -------------------------------------------------------------------------------- /test/functional/basic/index.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import whoAmI from './whoami'; 4 | import ping from './ping'; 5 | 6 | export default (server: any, gitlab: any) => { 7 | // eslint-disable-line no-unused-vars 8 | 9 | describe('basic test endpoints', () => { 10 | whoAmI(server); 11 | ping(server); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /test/jest.config.functional.js: -------------------------------------------------------------------------------- 1 | /* eslint comma-dangle: 0 */ 2 | 3 | module.exports = { 4 | name: 'verdaccio-functional-jest', 5 | verbose: true, 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 7 | globalSetup: './functional/pre-setup.js', 8 | globalTeardown: './functional/teardown.js', 9 | testEnvironment: './functional/test-environment.js', 10 | collectCoverage: false, 11 | }; 12 | -------------------------------------------------------------------------------- /test/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createHash } from 'crypto'; 4 | 5 | import _ from 'lodash'; 6 | 7 | export const defaultTarballHashAlgorithm = 'sha1'; 8 | 9 | export const buildToken = (type: string, token: string) => { 10 | return `${_.capitalize(type)} ${token}`; 11 | }; 12 | 13 | export function createTarballHash() { 14 | return createHash(defaultTarballHashAlgorithm); 15 | } 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Roger Meier 2 | // SPDX-License-Identifier: MIT 3 | 4 | import { PluginOptions } from '@verdaccio/types'; 5 | 6 | import VerdaccioGitLab from './gitlab'; 7 | import { VerdaccioGitlabConfig } from './gitlab'; 8 | 9 | export default function(config: VerdaccioGitlabConfig, options: PluginOptions) { 10 | return new VerdaccioGitLab(config, options); 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch verdaccio", 8 | "program": "${workspaceFolder}/lib/verdaccio.js", 9 | "preLaunchTask": "npm: build", 10 | "env": { 11 | "NODE_PATH": "${workspaceFolder}/..", 12 | "NODE_ENV": "development" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/functional/fixtures/package.ts: -------------------------------------------------------------------------------- 1 | import {DOMAIN_SERVERS, PORT_SERVER_1, TARBALL} from '../config.functional'; 2 | 3 | export default function(name, version = '0.0.0', port = PORT_SERVER_1, domain= `http://${DOMAIN_SERVERS}:${port}`, 4 | fileName = TARBALL, readme = 'this is a readme'): any { 5 | return { 6 | name, 7 | version, 8 | readme, 9 | dist: { 10 | shasum: 'fake', 11 | tarball: `${domain}/${encodeURIComponent(name)}/-/${fileName}`, 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "strict": true, 8 | "outDir": "lib", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "typeRoots": [ 12 | "./node_modules/@verdaccio/types/lib/verdaccio", 13 | "./node_modules/@types" 14 | ] 15 | }, 16 | "include": [ 17 | "src/*.ts", 18 | "types/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/jest.config.unit.js: -------------------------------------------------------------------------------- 1 | /* eslint comma-dangle: 0 */ 2 | 3 | module.exports = { 4 | name: 'verdaccio-gitlab-unit-jest', 5 | verbose: true, 6 | rootDir: '..', 7 | collectCoverage: true, 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 9 | transform: { 10 | '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', 11 | }, 12 | testURL: 'http://localhost', 13 | collectCoverageFrom: ['src/*.{ts}'], 14 | testRegex: 'test/unit/.*\\.spec\\.ts', 15 | coveragePathIgnorePatterns: ['node_modules', 'fixtures'], 16 | }; 17 | -------------------------------------------------------------------------------- /test/lib/verdaccio-server.ts: -------------------------------------------------------------------------------- 1 | import { IVerdaccioConfig } from '../types'; 2 | 3 | export class VerdaccioConfig implements IVerdaccioConfig { 4 | public storagePath: string; 5 | public configPath: string; 6 | public domainPath: string; 7 | public port: number; 8 | 9 | public constructor(storagePath: string, configPath: string, domainPath: string, port: number) { 10 | this.storagePath = storagePath; 11 | this.configPath = configPath; 12 | this.domainPath = domainPath; 13 | this.port = port; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /conf/docker.yaml: -------------------------------------------------------------------------------- 1 | storage: /verdaccio/storage/data 2 | 3 | plugins: /verdaccio/plugins 4 | 5 | listen: 6 | - 0.0.0.0:4873 7 | 8 | auth: 9 | gitlab: 10 | url: http://gitlab 11 | authCache: 12 | enabled: true 13 | ttl: 300 14 | publish: $maintainer 15 | 16 | uplinks: 17 | npmjs: 18 | url: https://registry.npmjs.org/ 19 | 20 | packages: 21 | '@*/*': 22 | # scoped packages 23 | access: $all 24 | publish: $authenticated 25 | proxy: npmjs 26 | gitlab: true 27 | 28 | '**': 29 | access: $all 30 | publish: $authenticated 31 | proxy: npmjs 32 | gitlab: true 33 | 34 | logs: 35 | - { type: stdout, format: pretty, level: info } 36 | #- {type: file, path: verdaccio.log, level: info} 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | gitlab: 5 | image: 'gitlab/gitlab-ce:nightly' 6 | restart: always 7 | environment: 8 | - GITLAB_ROOT_PASSWORD=verdaccio 9 | ports: 10 | - '50080:80' 11 | - '50022:22' 12 | volumes: 13 | - gitlab-config:/etc/gitlab 14 | - gitlab-log:/var/log/gitlab 15 | - gitlab-data:/var/opt/gitlab 16 | 17 | verdaccio: 18 | restart: always 19 | ports: 20 | - '4873:4873' 21 | build: 22 | context: . 23 | dockerfile: Dockerfile 24 | volumes: 25 | - verdaccio-storage:/verdaccio/storage 26 | links: 27 | - gitlab 28 | 29 | volumes: 30 | gitlab-config: 31 | gitlab-log: 32 | gitlab-data: 33 | verdaccio-storage: 34 | -------------------------------------------------------------------------------- /test/functional/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { IServerBridge } from '../types'; 2 | 3 | import basic from './basic'; 4 | import auth from './auth'; 5 | import access from './access'; 6 | import publish from './publish'; 7 | 8 | describe('Functional Tests verdaccio-gitlab', () => { 9 | jest.setTimeout(10000); 10 | const server1: IServerBridge = global.__SERVERS__[0]; 11 | const gitlab = global.__GITLAB_SERVER__.app; 12 | 13 | // list of tests 14 | // note: order of the following calls is important 15 | basic(server1, gitlab); 16 | auth(server1, gitlab); 17 | access(server1, gitlab); 18 | publish(server1, gitlab); 19 | }); 20 | 21 | process.on('unhandledRejection', err => { 22 | console.error('unhandledRejection', err); 23 | process.nextTick(() => { 24 | throw err; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/__mocks__/gitlab.js: -------------------------------------------------------------------------------- 1 | const mock = jest.fn().mockImplementation(() => { 2 | return { 3 | Users: { 4 | current: () => { 5 | return Promise.resolve({ 6 | username: 'myUser', 7 | }); 8 | }, 9 | }, 10 | Groups: { 11 | all: params => { 12 | // eslint-disable-line no-unused-vars 13 | return Promise.resolve([ 14 | { 15 | path: 'myGroup', 16 | full_path: 'myGroup', 17 | }, 18 | ]); 19 | }, 20 | }, 21 | Projects: { 22 | all: params => { 23 | // eslint-disable-line no-unused-vars 24 | return Promise.resolve([ 25 | { 26 | path_with_namespace: 'anotherGroup/myProject', 27 | }, 28 | ]); 29 | }, 30 | }, 31 | }; 32 | }); 33 | 34 | export default mock; 35 | -------------------------------------------------------------------------------- /test/functional/store/config-1.yaml: -------------------------------------------------------------------------------- 1 | storage: ./test-storage1 2 | 3 | max_users: 2 4 | 5 | web: 6 | enable: true 7 | title: verdaccio-gitlab-server-1 8 | 9 | auth: 10 | gitlab: 11 | url: http://localhost:50080/ 12 | authCache: 13 | enabled: false 14 | publish: $maintainer 15 | 16 | uplinks: 17 | npmjs: 18 | url: https://registry.npmjs.org/ 19 | 20 | logs: 21 | - { type: stdout, format: pretty, level: trace } 22 | 23 | packages: 24 | 'verdaccio': 25 | access: $authenticated 26 | publish: $authenticated 27 | storage: false 28 | gitlab: true 29 | proxy: npmjs 30 | 31 | '@*/*': 32 | access: $all 33 | publish: $all 34 | gitlab: true 35 | 36 | '**': 37 | access: $all 38 | publish: $authenticated 39 | gitlab: true 40 | 41 | # expose internal methods 42 | _debug: true 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Roger Meier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/functional/fixtures/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__NAME__", 3 | "versions": { 4 | "0.1.0": { 5 | "name": "__NAME__", 6 | "version": "0.1.0", 7 | "dist": { 8 | "shasum": "fake", 9 | "tarball": "http://localhost:55551/__NAME__/-/blahblah" 10 | } 11 | }, 12 | "0.1.1alpha": { 13 | "name": "__NAME__", 14 | "version": "0.1.1alpha", 15 | "dist": { 16 | "shasum": "fake", 17 | "tarball": "http://localhost:55551/__NAME__/-/blahblah" 18 | } 19 | }, 20 | "0.1.2": { 21 | "name": "__NAME__", 22 | "version": "0.1.2", 23 | "dist": { 24 | "shasum": "fake", 25 | "tarball": "http://localhost:55551/__NAME__/-/blahblah" 26 | } 27 | }, 28 | "0.1.3alpha": { 29 | "name": "__NAME__", 30 | "version": "0.1.3alpha", 31 | "dist": { 32 | "shasum": "fake", 33 | "tarball": "http://localhost:55551/__NAME__/-/blahblah" 34 | } 35 | }, 36 | "1.1.0": { 37 | "name": "__NAME__", 38 | "version": "1.1.0", 39 | "dist": { 40 | "shasum": "fake", 41 | "tarball": "http://localhost:55551/__NAME__/-/blahblah" 42 | } 43 | } 44 | }, 45 | "dist-tags": { 46 | "latest": "1.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/functional/access/index.ts: -------------------------------------------------------------------------------- 1 | import { CREDENTIALS, PACKAGE } from '../config.functional'; 2 | import { API_ERROR, HTTP_STATUS } from '../../lib/constants'; 3 | 4 | export default (server: any, _gitlab: any) => { 5 | // eslint-disable-line no-unused-vars 6 | 7 | describe('package access tests', () => { 8 | beforeEach(() => { 9 | return server.auth(CREDENTIALS.user, CREDENTIALS.password); 10 | }); 11 | 12 | test('should allow access to an existing proxied package', () => { 13 | return server 14 | .getPackage(PACKAGE.EXISTING_NAME) 15 | .status(HTTP_STATUS.OK) 16 | .then(body => { 17 | expect(body).toHaveProperty('name'); 18 | expect(body.name).toBe(PACKAGE.EXISTING_NAME); 19 | }); 20 | }); 21 | 22 | test('should fail with non-existing package', () => { 23 | return server 24 | .getPackage(PACKAGE.NON_EXISTING_NAME) 25 | .status(HTTP_STATUS.NOT_FOUND) 26 | .body_error(API_ERROR.NO_PACKAGE) 27 | .then(body => { 28 | expect(body).toHaveProperty('error'); 29 | expect(body.error).toMatch(/no/); 30 | }); 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /test/unit/partials/config/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginOptions, RemoteUser } from '@verdaccio/types'; 2 | 3 | import { VerdaccioGitlabConfig } from '../../../../src/gitlab'; 4 | import { UserDataGroups } from '../../../../src/authcache'; 5 | import logger from '../logger'; 6 | import { UserData } from '../../../../src/authcache'; 7 | 8 | 9 | const verdaccioGitlabConfig: VerdaccioGitlabConfig = { 10 | url: 'myUrl' 11 | }; 12 | 13 | const options: PluginOptions = { 14 | config: {}, 15 | logger: logger 16 | }; 17 | 18 | const user = 'myUser'; 19 | const pass = 'myPass'; 20 | const remoteUser: RemoteUser = { 21 | real_groups: ['myGroup', 'anotherGroup/myProject', user], 22 | groups: ['myGroup', 'anotherGroup/myProject', user], 23 | name: user 24 | }; 25 | 26 | const userDataGroups: UserDataGroups = { 27 | publish: ['fooGroup1', 'fooGroup2'] 28 | }; 29 | const userData: UserData = new UserData(user, userDataGroups); 30 | 31 | const config = { 32 | verdaccioGitlabConfig: verdaccioGitlabConfig, 33 | options: options, 34 | user: user, 35 | pass: pass, 36 | remoteUser: remoteUser, 37 | userData: userData 38 | } 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.3-alpine3.11 as builder 2 | 3 | WORKDIR /opt/verdaccio-gitlab-build 4 | COPY . . 5 | 6 | ENV NODE_ENV=production \ 7 | VERDACCIO_BUILD_REGISTRY=https://registry.npmjs.org/ 8 | 9 | RUN yarn config set registry $VERDACCIO_BUILD_REGISTRY && \ 10 | yarn install --production=false && \ 11 | yarn code:docker-build && \ 12 | yarn cache clean && \ 13 | yarn install --production=true --pure-lockfile 14 | 15 | 16 | 17 | FROM verdaccio/verdaccio:4 18 | LABEL maintainer="https://github.com/bufferoverflow/verdaccio-gitlab" 19 | 20 | # Go back to root to be able to install the plugin 21 | USER root 22 | 23 | COPY --from=builder /opt/verdaccio-gitlab-build/build /opt/verdaccio-gitlab/build 24 | COPY --from=builder /opt/verdaccio-gitlab-build/package.json /opt/verdaccio-gitlab/package.json 25 | COPY --from=builder /opt/verdaccio-gitlab-build/node_modules /opt/verdaccio-gitlab/node_modules 26 | 27 | ADD conf/docker.yaml /verdaccio/conf/config.yaml 28 | 29 | # Inherited from parent image 30 | WORKDIR $VERDACCIO_APPDIR 31 | RUN ln -s /opt/verdaccio-gitlab/build /verdaccio/plugins/verdaccio-gitlab 32 | 33 | # Inherited from parent image 34 | USER $VERDACCIO_USER_UID 35 | -------------------------------------------------------------------------------- /test/lib/constants.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const TOKEN_BASIC = 'Basic'; 4 | 5 | export const HEADERS = { 6 | JSON: 'application/json', 7 | CONTENT_TYPE: 'Content-type', 8 | CONTENT_LENGTH: 'content-length', 9 | JSON_CHARSET: 'application/json; charset=utf-8', 10 | OCTET_STREAM: 'application/octet-stream; charset=utf-8', 11 | }; 12 | 13 | export const API_MESSAGE = { 14 | PKG_CREATED: 'created new package', 15 | PKG_CHANGED: 'package changed', 16 | PKG_REMOVED: 'package removed', 17 | PKG_PUBLISHED: 'package published', 18 | TARBALL_UPLOADED: 'tarball uploaded successfully', 19 | TARBALL_REMOVED: 'tarball removed', 20 | TAG_UPDATED: 'tags updated', 21 | TAG_REMOVED: 'tag removed', 22 | TAG_ADDED: 'package tagged', 23 | LOGGED_OUT: 'Logged out', 24 | }; 25 | 26 | export const HTTP_STATUS = { 27 | OK: 200, 28 | CREATED: 201, 29 | MULTIPLE_CHOICES: 300, 30 | NOT_MODIFIED: 304, 31 | BAD_REQUEST: 400, 32 | UNAUTHORIZED: 401, 33 | FORBIDDEN: 403, 34 | NOT_FOUND: 404, 35 | CONFLICT: 409, 36 | UNSUPORTED_MEDIA: 415, 37 | BAD_DATA: 422, 38 | INTERNAL_ERROR: 500, 39 | SERVICE_UNAVAILABLE: 503, 40 | LOOP_DETECTED: 508, 41 | }; 42 | 43 | export const API_ERROR = { 44 | NO_PACKAGE: 'no such package available', 45 | }; 46 | 47 | export const GITLAB = { 48 | UNAUTHENTICATED: { 49 | message: '401 Unauthorized', 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /test/functional/auth/index.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CREDENTIALS, WRONG_CREDENTIALS } from '../config.functional'; 4 | import { HTTP_STATUS } from '../../lib/constants'; 5 | 6 | export default (server: any, gitlab: any) => { 7 | // eslint-disable-line no-unused-vars 8 | 9 | describe('authentication tests', () => { 10 | test('should authenticate user', () => { 11 | return server 12 | .auth(CREDENTIALS.user, CREDENTIALS.password) 13 | .status(HTTP_STATUS.CREATED) 14 | .body_ok(new RegExp(CREDENTIALS.user)) 15 | .then(body => { 16 | expect(body).toHaveProperty('ok'); 17 | expect(body).toHaveProperty('token'); 18 | }); 19 | }); 20 | 21 | test('should fail authentication with wrong user', () => { 22 | return server 23 | .auth(WRONG_CREDENTIALS.user, CREDENTIALS.password) 24 | .status(HTTP_STATUS.UNAUTHORIZED) 25 | .then(body => { 26 | expect(body).toHaveProperty('error'); 27 | expect(body.error).toMatch(/wrong/); 28 | }); 29 | }); 30 | 31 | test('should fail authentication with wrong password', () => { 32 | return server 33 | .auth(CREDENTIALS.user, WRONG_CREDENTIALS.password) 34 | .status(HTTP_STATUS.UNAUTHORIZED) 35 | .then(body => { 36 | expect(body).toHaveProperty('error'); 37 | expect(body.error).toMatch(/error/); 38 | }); 39 | }); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /test/unit/authcache.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthCache, UserData } from '../../src/authcache'; 2 | 3 | import logger from './partials/logger'; 4 | import config from './partials/config'; 5 | 6 | describe('AuthCache Unit Tests', () => { 7 | test('should create an AuthCache instance', () => { 8 | const authCache: AuthCache = new AuthCache(logger, AuthCache.DEFAULT_TTL); 9 | 10 | expect(authCache).toBeTruthy(); 11 | }); 12 | 13 | test('should create an AuthCache instance with default ttl', () => { 14 | const authCache: AuthCache = new AuthCache(logger); 15 | 16 | expect(authCache).toBeTruthy(); 17 | expect(authCache).toHaveProperty('ttl', AuthCache.DEFAULT_TTL); 18 | }); 19 | 20 | test('should store and find some user data', () => { 21 | const authCache: AuthCache = new AuthCache(logger); 22 | 23 | authCache.storeUser(config.user, config.pass, config.userData); 24 | const returnedData: UserData = authCache.findUser(config.user, config.pass); 25 | 26 | expect(returnedData).toEqual(config.userData); 27 | }); 28 | 29 | test('should store and find some user data when ttl is unlimited', () => { 30 | const UNLIMITED_TTL = 0; 31 | const authCache: AuthCache = new AuthCache(logger, UNLIMITED_TTL); 32 | 33 | authCache.storeUser(config.user, config.pass, config.userData); 34 | const returnedData: UserData = authCache.findUser(config.user, config.pass); 35 | 36 | expect(returnedData).toEqual(config.userData); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IVerdaccioConfig { 2 | storagePath: string; 3 | configPath: string; 4 | domainPath: string; 5 | port: number | string; 6 | } 7 | 8 | export interface IRequestPromise { 9 | status(reason: any): any; 10 | body_ok(reason: any): any; 11 | body_error(reason: any): any; 12 | request(reason: any): any; 13 | response(reason: any): any; 14 | send(reason: any): any; 15 | } 16 | 17 | export interface IServerProcess { 18 | init(): Promise; 19 | stop(): void; 20 | } 21 | 22 | declare class PromiseAssert extends Promise { 23 | public constructor(options: any); 24 | } 25 | 26 | export interface IServerBridge { 27 | url: string; 28 | userAgent: string; 29 | authstr: string; 30 | request(options: any): typeof PromiseAssert; 31 | auth(name: string, password: string): IRequestPromise; 32 | auth(name: string, password: string): IRequestPromise; 33 | logout(token: string): Promise; 34 | getPackage(name: string): Promise; 35 | putPackage(name: string, data: any): Promise; 36 | putVersion(name: string, version: string, data: any): Promise; 37 | getTarball(name: string, filename: string): Promise; 38 | putTarball(name: string, filename: string, data: any): Promise; 39 | removeTarball(name: string): Promise; 40 | removeSingleTarball(name: string, filename: string): Promise; 41 | addTag(name: string, tag: string, version: string): Promise; 42 | putTarballIncomplete(name: string, filename: string, data: any, size: number, cb: Function): Promise; 43 | addPackage(name: string): Promise; 44 | whoami(): Promise; 45 | ping(): Promise; 46 | debug(): IRequestPromise; 47 | } 48 | -------------------------------------------------------------------------------- /test/functional/lib/mock_gitlab_server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import chalk from 'chalk'; 4 | 5 | import { GITLAB_DATA, CREDENTIALS } from '../config.functional'; 6 | import { GITLAB, HTTP_STATUS } from '../../lib/constants'; 7 | 8 | export default class GitlabServer { 9 | private app: any; 10 | private server: any; 11 | private expectedToken: string; 12 | 13 | public constructor() { 14 | this.app = express(); 15 | this.expectedToken = CREDENTIALS.password; 16 | } 17 | 18 | public start(port: number): Promise { 19 | return new Promise(resolve => { 20 | this.app.use(bodyParser.json()); 21 | this.app.use( 22 | bodyParser.urlencoded({ 23 | extended: true, 24 | }) 25 | ); 26 | 27 | this.app.get('/api/v4/user', (req, res) => { 28 | this._checkAuthentication(req, res, () => { 29 | res.send(GITLAB_DATA.testUser); 30 | }); 31 | }); 32 | 33 | this.app.get('/api/v4/groups', (req, res) => { 34 | this._checkAuthentication(req, res, () => { 35 | res.send(GITLAB_DATA.testUserGroups); 36 | }); 37 | }); 38 | 39 | this.app.get('/api/v4/projects', (req, res) => { 40 | this._checkAuthentication(req, res, () => { 41 | res.send(GITLAB_DATA.testUserProjects); 42 | }); 43 | }); 44 | 45 | this.server = this.app.listen(port, () => { 46 | console.log(chalk.blue(`gitlab mock listening on port: ${port}`)); 47 | resolve(this); 48 | }); 49 | console.log(chalk.blue(`Running gitlab mock server via express`)); 50 | }); 51 | } 52 | 53 | private _checkAuthentication(req: any, res: any, cb: any) { 54 | if (req.get('private-token') === this.expectedToken) { 55 | cb(); 56 | } else { 57 | res.status(HTTP_STATUS.UNAUTHORIZED).send(GITLAB.UNAUTHENTICATED); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/authcache.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Roger Meier 2 | // SPDX-License-Identifier: MIT 3 | 4 | import Crypto from 'crypto'; 5 | 6 | import { Logger } from '@verdaccio/types'; 7 | import NodeCache from 'node-cache'; 8 | 9 | export class AuthCache { 10 | private logger: Logger; 11 | private ttl: number; 12 | private storage: NodeCache; 13 | 14 | public static get DEFAULT_TTL() { 15 | return 300; 16 | } 17 | 18 | private static _generateKeyHash(username: string, password: string) { 19 | const sha = Crypto.createHash('sha256'); 20 | sha.update(JSON.stringify({ username: username, password: password })); 21 | return sha.digest('hex'); 22 | } 23 | 24 | public constructor(logger: Logger, ttl?: number) { 25 | this.logger = logger; 26 | this.ttl = ttl || AuthCache.DEFAULT_TTL; 27 | 28 | this.storage = new NodeCache({ 29 | stdTTL: this.ttl, 30 | useClones: false, 31 | }); 32 | this.storage.on('expired', (key, value) => { 33 | this.logger.trace(`[gitlab] expired key: ${key} with value:`, value); 34 | }); 35 | } 36 | 37 | public findUser(username: string, password: string): UserData { 38 | return this.storage.get(AuthCache._generateKeyHash(username, password)) as UserData; 39 | } 40 | 41 | public storeUser(username: string, password: string, userData: UserData): boolean { 42 | return this.storage.set(AuthCache._generateKeyHash(username, password), userData); 43 | } 44 | } 45 | 46 | export type UserDataGroups = { 47 | publish: string[]; 48 | }; 49 | 50 | export class UserData { 51 | private _username: string; 52 | private _groups: UserDataGroups; 53 | 54 | public get username(): string { 55 | return this._username; 56 | } 57 | public get groups(): UserDataGroups { 58 | return this._groups; 59 | } 60 | public set groups(groups: UserDataGroups) { 61 | this._groups = groups; 62 | } 63 | 64 | public constructor(username: string, groups: UserDataGroups) { 65 | this._username = username; 66 | this._groups = groups; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/functional/config.functional.ts: -------------------------------------------------------------------------------- 1 | import { DOMAIN_SERVERS as localhost } from '../test.conf'; 2 | 3 | export const CREDENTIALS = { 4 | user: 'test', 5 | password: 'test', 6 | email: 'test@example.com', 7 | }; 8 | 9 | export const WRONG_CREDENTIALS = { 10 | user: 'non_existing_user', 11 | password: 'wrong_password', 12 | }; 13 | 14 | export const TARBALL = 'tarball-blahblah-file.name'; 15 | export const PORT_GITLAB_EXPRESS_MOCK = '50080'; 16 | export const PORT_SERVER_1 = '55551'; 17 | 18 | export const DOMAIN_SERVERS = localhost; 19 | 20 | export const PACKAGE = { 21 | EXISTING_NAME: 'verdaccio', 22 | NON_EXISTING_NAME: 'non-existing-package', 23 | NAME: 'test-name', 24 | GROUP_NAME: 'test-group', 25 | SCOPED_GROUP_NAME: '@test-group/another-project', 26 | SCOPED_PROJECT_NAME: '@another-group/test-project', 27 | VERSION: '0.0.1', 28 | }; 29 | 30 | export const GITLAB_DATA = { 31 | testUser: { 32 | id: 1, 33 | username: CREDENTIALS.user, 34 | email: CREDENTIALS.email, 35 | name: 'Test User', 36 | state: 'active', 37 | }, 38 | testUserGroups: [ 39 | { 40 | id: 1, 41 | name: 'Test Group', 42 | path: 'test-group', 43 | description: 'An interesting group', 44 | visibility: 'public', 45 | lfs_enabled: true, 46 | request_access_enabled: false, 47 | full_path: 'test-group', 48 | parent_id: null, 49 | }, 50 | ], 51 | testUserProjects: [ 52 | { 53 | id: 3, 54 | name: 'Test Project', 55 | name_with_namespace: 'Another Group / Test Project', 56 | path: 'test-project', 57 | path_with_namespace: 'another-group/test-project', 58 | namespace: { 59 | id: 2, 60 | name: 'Another Group', 61 | path: 'another-group', 62 | kind: 'group', 63 | full_path: 'another-group', 64 | parent_id: null, 65 | }, 66 | description: 'An interesting project group', 67 | visibility: 'public', 68 | lfs_enabled: true, 69 | request_access_enabled: false, 70 | parent_id: null, 71 | }, 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /test/functional/publish/index.ts: -------------------------------------------------------------------------------- 1 | import { CREDENTIALS, PACKAGE, WRONG_CREDENTIALS } from '../config.functional'; 2 | import { HTTP_STATUS } from '../../lib/constants'; 3 | import fixturePkg from '../fixtures/package'; 4 | 5 | export default (server: any, gitlab: any) => { 6 | describe('package publish tests', () => { 7 | beforeEach(() => { 8 | return server.auth(CREDENTIALS.user, CREDENTIALS.password); 9 | }); 10 | 11 | test('should deny publish of package when unauthenticated', () => { 12 | return server.auth(WRONG_CREDENTIALS.user, CREDENTIALS.password).then(() => { 13 | return server 14 | .putPackage(PACKAGE.NAME, fixturePkg(PACKAGE.NAME)) 15 | .status(HTTP_STATUS.FORBIDDEN) 16 | .then(body => { 17 | expect(body).toHaveProperty('error'); 18 | }); 19 | }); 20 | }); 21 | 22 | test('should allow publish of package when gitlab groups match', () => { 23 | return server 24 | .putPackage(PACKAGE.GROUP_NAME, fixturePkg(PACKAGE.GROUP_NAME)) 25 | .status(HTTP_STATUS.CREATED) 26 | .body_ok(/created new package/) 27 | .then(body => { 28 | expect(body).toHaveProperty('ok'); 29 | expect(body.ok).toMatch(/created/); 30 | expect(body).toHaveProperty('success'); 31 | expect(body.success).toBe(true); 32 | }); 33 | }); 34 | 35 | test('should allow publish of scoped package when gitlab groups match', () => { 36 | return server 37 | .putPackage(PACKAGE.SCOPED_GROUP_NAME, fixturePkg(PACKAGE.SCOPED_GROUP_NAME)) 38 | .status(HTTP_STATUS.CREATED) 39 | .body_ok(/created new package/) 40 | .then(body => { 41 | expect(body).toHaveProperty('ok'); 42 | expect(body.ok).toMatch(/created/); 43 | expect(body).toHaveProperty('success'); 44 | expect(body.success).toBe(true); 45 | }); 46 | }); 47 | 48 | test('should allow publish of scoped package when gitlab projects match', () => { 49 | return server 50 | .putPackage(PACKAGE.SCOPED_PROJECT_NAME, fixturePkg(PACKAGE.SCOPED_PROJECT_NAME)) 51 | .status(HTTP_STATUS.CREATED) 52 | .body_ok(/created new package/) 53 | .then(body => { 54 | expect(body).toHaveProperty('ok'); 55 | expect(body.ok).toMatch(/created/); 56 | expect(body).toHaveProperty('success'); 57 | expect(body.success).toBe(true); 58 | }); 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /test/functional/lib/environment.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { yellow, green, blue, magenta } from 'kleur'; 4 | import NodeEnvironment from 'jest-environment-node'; 5 | 6 | import { VerdaccioConfig } from '../../lib/verdaccio-server'; 7 | import VerdaccioProcess from '../../lib/server_process'; 8 | import Server from '../../lib/server'; 9 | import { IServerBridge } from '../../types'; 10 | import { PORT_GITLAB_EXPRESS_MOCK, DOMAIN_SERVERS, PORT_SERVER_1 } from '../config.functional'; 11 | 12 | import GitlabServer from './mock_gitlab_server'; 13 | 14 | class FunctionalEnvironment extends NodeEnvironment { 15 | private config: any; 16 | 17 | public constructor(config: any) { 18 | super(config); 19 | } 20 | 21 | public async startWeb() { 22 | const gitlab: any = new GitlabServer(); 23 | 24 | return await gitlab.start(PORT_GITLAB_EXPRESS_MOCK); 25 | } 26 | 27 | public async setup() { 28 | const SILENCE_LOG = !process.env.VERDACCIO_DEBUG; 29 | // @ts-ignore 30 | const DEBUG_INJECT: boolean = process.env.VERDACCIO_DEBUG_INJECT ? process.env.VERDACCIO_DEBUG_INJECT : false; 31 | const forkList: any[] = []; 32 | const serverList: IServerBridge[] = []; 33 | const pathStore = path.join(__dirname, '../store'); 34 | const listServers = [ 35 | { 36 | port: PORT_SERVER_1, 37 | config: '/config-1.yaml', 38 | storage: '/test-storage1', 39 | } 40 | ]; 41 | console.log(green('Setup Verdaccio Servers')); 42 | 43 | const app = await this.startWeb(); 44 | this.global.__GITLAB_SERVER__ = app; 45 | 46 | for (const config of listServers) { 47 | const verdaccioConfig = new VerdaccioConfig( 48 | path.join(pathStore, config.storage), 49 | path.join(pathStore, config.config), 50 | `http://${DOMAIN_SERVERS}:${config.port}/`, config.port); 51 | console.log(magenta(`Running registry ${config.config} on port ${config.port}`)); 52 | const server: IServerBridge = new Server(verdaccioConfig.domainPath); 53 | serverList.push(server); 54 | const process = new VerdaccioProcess(verdaccioConfig, server, SILENCE_LOG, DEBUG_INJECT); 55 | 56 | const fork = await process.init(); 57 | console.log(blue(`Fork PID ${fork[1]}`)); 58 | forkList.push(fork); 59 | } 60 | 61 | this.global.__SERVERS_PROCESS__ = forkList; 62 | this.global.__SERVERS__ = serverList; 63 | } 64 | 65 | public async teardown() { 66 | await super.teardown(); 67 | console.log(yellow('Teardown Test Environment.')); 68 | 69 | if (!this.global.__SERVERS_PROCESS__) { 70 | throw new Error("There are no servers to stop"); 71 | } 72 | 73 | // shutdown verdaccio 74 | for (const server of this.global.__SERVERS_PROCESS__) { 75 | server[0].stop(); 76 | } 77 | // close web server 78 | this.global.__GITLAB_SERVER__.server.close(); 79 | } 80 | 81 | private runScript(script: string) { 82 | return super.runScript(script); 83 | } 84 | } 85 | 86 | module.exports = FunctionalEnvironment; 87 | -------------------------------------------------------------------------------- /test/lib/server_process.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fork } from 'child_process'; 3 | 4 | import rimRaf from 'rimraf'; 5 | import _ from 'lodash'; 6 | 7 | import { CREDENTIALS } from '../functional/config.functional'; 8 | import { IVerdaccioConfig, IServerBridge, IServerProcess } from '../types'; 9 | 10 | import { HTTP_STATUS } from './constants'; 11 | 12 | export default class VerdaccioProcess implements IServerProcess { 13 | private bridge: IServerBridge; 14 | private config: IVerdaccioConfig; 15 | private childFork: any; 16 | private isDebug: boolean; 17 | private silence: boolean; 18 | private cleanStore: boolean; 19 | 20 | public constructor( 21 | config: IVerdaccioConfig, 22 | bridge: IServerBridge, 23 | silence = true, 24 | isDebug = false, 25 | cleanStore = true 26 | ) { 27 | this.config = config; 28 | this.bridge = bridge; 29 | this.silence = silence; 30 | this.isDebug = isDebug; 31 | this.cleanStore = cleanStore; 32 | } 33 | 34 | public init(verdaccioPath = '../../bin/verdaccio'): Promise { 35 | return new Promise((resolve, reject) => { 36 | if (this.cleanStore) { 37 | rimRaf(this.config.storagePath, err => { 38 | if (_.isNil(err) === false) { 39 | reject(err); 40 | } 41 | 42 | this._start(verdaccioPath, resolve, reject); 43 | }); 44 | } else { 45 | this._start(verdaccioPath, resolve, reject); 46 | } 47 | }); 48 | } 49 | 50 | private _start(verdaccioPath: string, resolve: Function, reject: Function) { 51 | const verdaccioRegisterWrap: string = path.join(__dirname, verdaccioPath); 52 | let childOptions = { 53 | silent: true, 54 | }; 55 | 56 | if (this.isDebug) { 57 | const debugPort = parseInt(this.config.port, 10) + 5; 58 | 59 | childOptions = Object.assign({}, childOptions, { 60 | execArgv: [`--inspect=${debugPort}`], 61 | }); 62 | } 63 | 64 | const { configPath, port } = this.config; 65 | this.childFork = fork(verdaccioRegisterWrap, ['-c', configPath, '-l', port as string], childOptions); 66 | 67 | this.childFork.on('message', msg => { 68 | // verdaccio_started is a message that comes from verdaccio in debug mode that notify has been started 69 | if ('verdaccio_started' in msg) { 70 | this.bridge 71 | .debug() 72 | .status(HTTP_STATUS.OK) 73 | .then(body => { 74 | this.bridge 75 | .auth(CREDENTIALS.user, CREDENTIALS.password) 76 | .status(HTTP_STATUS.CREATED) 77 | .body_ok(new RegExp(CREDENTIALS.user)) 78 | .then(() => resolve([this, body.pid]), reject); 79 | }, reject); 80 | } 81 | }); 82 | 83 | this.childFork.on('error', err => reject([err, this])); 84 | this.childFork.on('disconnect', err => reject([err, this])); 85 | this.childFork.on('exit', err => reject([err, this])); 86 | } 87 | 88 | public stop(): void { 89 | return this.childFork.kill('SIGINT'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/lib/request.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import _ from 'lodash'; 4 | import request from 'request'; 5 | 6 | import { IRequestPromise } from '../types'; 7 | 8 | const requestData = Symbol('smart_request_data'); 9 | 10 | function injectResponse(smartObject: any, promise: Promise): Promise { 11 | promise[requestData] = smartObject[requestData]; 12 | return promise; 13 | } 14 | 15 | export class PromiseAssert extends Promise implements IRequestPromise { 16 | public constructor(options: any) { 17 | super(options); 18 | } 19 | 20 | public status(expected: number) { 21 | const selfData = this[requestData]; 22 | 23 | return injectResponse( 24 | this, 25 | this.then(function(body) { 26 | try { 27 | assert.equal(selfData.response.statusCode, expected); 28 | } catch (err) { 29 | selfData.error.message = err.message; 30 | throw selfData.error; 31 | } 32 | return body; 33 | }) 34 | ); 35 | } 36 | 37 | public body_ok(expected: any) { 38 | const selfData = this[requestData]; 39 | 40 | return injectResponse( 41 | this, 42 | this.then(function(body) { 43 | try { 44 | if (_.isRegExp(expected)) { 45 | assert(body.ok.match(expected), "'" + body.ok + "' doesn't match " + expected); 46 | } else { 47 | assert.equal(body.ok, expected); 48 | } 49 | assert.equal(body.error, null); 50 | } catch (err) { 51 | selfData.error.message = err.message; 52 | throw selfData.error; 53 | } 54 | 55 | return body; 56 | }) 57 | ); 58 | } 59 | 60 | public body_error(expected: any) { 61 | const selfData = this[requestData]; 62 | 63 | return injectResponse( 64 | this, 65 | this.then(function(body) { 66 | try { 67 | if (_.isRegExp(expected)) { 68 | assert(body.error.match(expected), body.error + " doesn't match " + expected); 69 | } else { 70 | assert.equal(body.error, expected); 71 | } 72 | assert.equal(body.ok, null); 73 | } catch (err) { 74 | selfData.error.message = err.message; 75 | throw selfData.error; 76 | } 77 | return body; 78 | }) 79 | ); 80 | } 81 | 82 | public request(callback: any) { 83 | callback(this[requestData].request); 84 | return this; 85 | } 86 | 87 | public response(cb: any) { 88 | const selfData = this[requestData]; 89 | 90 | return injectResponse( 91 | this, 92 | this.then(function(body) { 93 | cb(selfData.response); 94 | return body; 95 | }) 96 | ); 97 | } 98 | 99 | public send(data: any) { 100 | this[requestData].request.end(data); 101 | return this; 102 | } 103 | } 104 | 105 | function smartRequest(options: any): Promise { 106 | const smartObject: any = {}; 107 | 108 | smartObject[requestData] = {}; 109 | smartObject[requestData].error = Error(); 110 | Error.captureStackTrace(smartObject[requestData].error, smartRequest); 111 | 112 | const promiseResult: Promise = new PromiseAssert(function(resolve, reject) { 113 | // store request reference on symbol 114 | smartObject[requestData].request = request(options, function(err, res, body) { 115 | if (err) { 116 | return reject(err); 117 | } 118 | 119 | // store the response on symbol 120 | smartObject[requestData].response = res; 121 | resolve(body); 122 | }); 123 | }); 124 | 125 | return injectResponse(smartObject, promiseResult); 126 | } 127 | 128 | export default smartRequest; 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "verdaccio-gitlab", 3 | "author": { 4 | "name": "Roger Meier", 5 | "email": "roger@bufferoverflow.ch" 6 | }, 7 | "scripts": { 8 | "type-check": "tsc --noEmit", 9 | "license": "license-checker --onlyAllow 'Apache-2.0; Apache License, Version 2.0; BSD; BSD-2-Clause; BSD-3-Clause; ISC; MIT; Unlicense; WTFPL; CC-BY-3.0; CC0-1.0' --production --summary", 10 | "type-check:watch": "npm run type-check -- --watch", 11 | "lint:ts": "eslint . --ext .js,.ts", 12 | "lint": "yarn type-check && yarn lint:ts && markdownlint README.md", 13 | "prepublish": "in-publish && yarn lint && yarn code:build || not-in-publish", 14 | "release:major": "changelog -M && git commit -a -m 'docs: updated CHANGELOG.md' && yarn version --major && git push origin && git push origin --tags", 15 | "release:minor": "changelog -m && git commit -a -m 'docs: updated CHANGELOG.md' && yarn version --minor && git push origin && git push origin --tags", 16 | "release:patch": "changelog -p && git commit -a -m 'docs: updated CHANGELOG.md' && yarn version --patch && git push origin && git push origin --tags", 17 | "start": "yarn code:build && cross-env NODE_PATH=$NODE_PATH:.. BABEL_ENV=registry babel-node build/verdaccio.js", 18 | "code:build:types": "tsc --emitDeclarationOnly", 19 | "code:build": "cross-env BABEL_ENV=registry babel src/ --out-dir build/ --extensions \".ts,.tsx\"", 20 | "code:docker-build": "cross-env BABEL_ENV=docker babel src/ --out-dir build/ --extensions \".ts,.tsx\"", 21 | "build:docker": "docker build -t verdaccio-gitlab . --no-cache", 22 | "test": "yarn test:unit", 23 | "test:unit": "cross-env BABEL_ENV=test TZ=UTC jest --config ./test/jest.config.unit.js --maxWorkers 2", 24 | "test:functional": "cross-env BABEL_ENV=test TZ=UTC jest --config ./test/jest.config.functional.js --testPathPattern ./test/functional/index* --passWithNoTests", 25 | "test:all": "yarn test && yarn test:functional" 26 | }, 27 | "main": "build/index.js", 28 | "version": "3.0.1", 29 | "description": "private npm registry (Verdaccio) using gitlab-ce as authentication and authorization provider", 30 | "keywords": [ 31 | "sinopia", 32 | "verdaccio", 33 | "gitlab", 34 | "auth", 35 | "npm", 36 | "registry", 37 | "npm-registry" 38 | ], 39 | "license": "MIT", 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/bufferoverflow/verdaccio-gitlab.git" 43 | }, 44 | "homepage": "https://github.com/bufferoverflow/verdaccio-gitlab", 45 | "bugs": { 46 | "url": "https://github.com/bufferoverflow/verdaccio-gitlab/issues" 47 | }, 48 | "engines": { 49 | "node": ">=10" 50 | }, 51 | "dependencies": { 52 | "gitlab": "3.5.1", 53 | "global-tunnel-ng": "2.5.3", 54 | "http-errors": "1.7.3", 55 | "node-cache": "4.2.0", 56 | "verdaccio": "^4.3.4" 57 | }, 58 | "devDependencies": { 59 | "@commitlint/cli": "^8.3.3", 60 | "@commitlint/config-conventional": "^8.3.3", 61 | "@commitlint/travis-cli": "^8.3.3", 62 | "@types/http-errors": "^1.6.3", 63 | "@types/jest": "^24.0.24", 64 | "@types/lodash": "^4.14.149", 65 | "@types/node": "^13.1.0", 66 | "@typescript-eslint/eslint-plugin": "^2.13.0", 67 | "@typescript-eslint/parser": "^2.13.0", 68 | "@verdaccio/babel-preset": "^8.5.0", 69 | "@verdaccio/commons-api": "^8.5.0", 70 | "@verdaccio/eslint-config": "^8.5.0", 71 | "@verdaccio/types": "^8.5.0", 72 | "body-parser": "^1.19.0", 73 | "cross-env": "^6.0.3", 74 | "eslint": "^6.8.0", 75 | "express": "^4.17.1", 76 | "generate-changelog": "^1.8.0", 77 | "http-status": "^1.4.2", 78 | "husky": "^3.1.0", 79 | "in-publish": "^2.0.0", 80 | "jest": "^24.9.0", 81 | "jest-environment-node": "^24.9.0", 82 | "kleur": "3.0.3", 83 | "license-checker": "^25.0.1", 84 | "lodash": "^4.17.15", 85 | "markdownlint-cli": "^0.20.0", 86 | "prettier": "^1.19.1", 87 | "request": "^2.88.0", 88 | "rimraf": "^3.0.0", 89 | "typescript": "^3.7.4" 90 | }, 91 | "commitlint": { 92 | "extends": [ 93 | "@commitlint/config-conventional" 94 | ] 95 | }, 96 | "husky": { 97 | "hooks": { 98 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 99 | } 100 | }, 101 | "eslintConfig": { 102 | "extends": [ 103 | "@verdaccio" 104 | ], 105 | "rules": { 106 | "@typescript-eslint/ban-ts-ignore": 0, 107 | "@typescript-eslint/no-explicit-any": 0, 108 | "@typescript-eslint/explicit-function-return-type": 0 109 | }, 110 | "ignorePatterns": [ 111 | "build/", 112 | "node_modules/" 113 | ] 114 | }, 115 | "babel": { 116 | "presets": [ 117 | [ 118 | "@verdaccio" 119 | ] 120 | ] 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/lib/server.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import _ from 'lodash'; 4 | 5 | import { IServerBridge } from '../types'; 6 | import { CREDENTIALS, DOMAIN_SERVERS, PORT_SERVER_1, TARBALL } from '../functional/config.functional'; 7 | 8 | import smartRequest from './request'; 9 | import { HEADERS, HTTP_STATUS, TOKEN_BASIC, API_MESSAGE } from './constants'; 10 | import { buildToken } from './utils'; 11 | 12 | const buildAuthHeader = (user, pass): string => { 13 | return buildToken(TOKEN_BASIC, new Buffer(`${user}:${pass}`).toString('base64')); 14 | }; 15 | 16 | export function getPackage( 17 | name, 18 | version = '0.0.0', 19 | port = PORT_SERVER_1, 20 | domain = `http://${DOMAIN_SERVERS}:${port}`, 21 | fileName = TARBALL, 22 | readme = 'this is a readme' 23 | ): any { 24 | return { 25 | name, 26 | version, 27 | readme, 28 | dist: { 29 | shasum: 'fake', 30 | tarball: `${domain}/${encodeURIComponent(name)}/-/${fileName}`, 31 | }, 32 | }; 33 | } 34 | 35 | export default class Server implements IServerBridge { 36 | public url: string; 37 | public userAgent: string; 38 | public authstr: string; 39 | 40 | public constructor(url: string) { 41 | this.url = url.replace(/\/$/, ''); 42 | this.userAgent = 'node/v8.1.2 linux x64'; 43 | this.authstr = buildAuthHeader(CREDENTIALS.user, CREDENTIALS.password); 44 | } 45 | 46 | public request(options: any): any { 47 | assert(options.uri); 48 | const headers = options.headers || {}; 49 | 50 | headers.accept = headers.accept || HEADERS.JSON; 51 | headers['user-agent'] = headers['user-agent'] || this.userAgent; 52 | headers.authorization = headers.authorization || this.authstr; 53 | 54 | return smartRequest({ 55 | url: this.url + options.uri, 56 | method: options.method || 'GET', 57 | headers: headers, 58 | encoding: options.encoding, 59 | json: _.isNil(options.json) === false ? options.json : true, 60 | }); 61 | } 62 | 63 | public auth(name: string, password: string) { 64 | this.authstr = buildAuthHeader(name, password); 65 | return this.request({ 66 | uri: `/-/user/org.couchdb.user:${encodeURIComponent(name)}/-rev/undefined`, 67 | method: 'PUT', 68 | json: { 69 | name, 70 | password, 71 | email: `${CREDENTIALS.user}@example.com`, 72 | _id: `org.couchdb.user:${name}`, 73 | type: 'user', 74 | roles: [], 75 | date: new Date(), 76 | }, 77 | }); 78 | } 79 | 80 | public logout(token: string) { 81 | return this.request({ 82 | uri: `/-/user/token/${encodeURIComponent(token)}`, 83 | method: 'DELETE', 84 | }); 85 | } 86 | 87 | public getPackage(name: string) { 88 | return this.request({ 89 | uri: `/${encodeURIComponent(name)}`, 90 | method: 'GET', 91 | }); 92 | } 93 | 94 | public putPackage(name: string, data) { 95 | if (_.isObject(data) && !Buffer.isBuffer(data)) { 96 | data = JSON.stringify(data); 97 | } 98 | 99 | return this.request({ 100 | uri: `/${encodeURIComponent(name)}`, 101 | method: 'PUT', 102 | headers: { 103 | [HEADERS.CONTENT_TYPE]: HEADERS.JSON, 104 | }, 105 | }).send(data); 106 | } 107 | 108 | public putVersion(name: string, version: string, data: any) { 109 | if (_.isObject(data) && !Buffer.isBuffer(data)) { 110 | data = JSON.stringify(data); 111 | } 112 | 113 | return this.request({ 114 | uri: `/${encodeURIComponent(name)}/${encodeURIComponent(version)}/-tag/latest`, 115 | method: 'PUT', 116 | headers: { 117 | [HEADERS.CONTENT_TYPE]: HEADERS.JSON, 118 | }, 119 | }).send(data); 120 | } 121 | 122 | public getTarball(name: string, filename: string) { 123 | return this.request({ 124 | uri: `/${encodeURIComponent(name)}/-/${encodeURIComponent(filename)}`, 125 | method: 'GET', 126 | encoding: null, 127 | }); 128 | } 129 | 130 | public putTarball(name: string, filename: string, data: any) { 131 | return this.request({ 132 | uri: `/${encodeURIComponent(name)}/-/${encodeURIComponent(filename)}/whatever`, 133 | method: 'PUT', 134 | headers: { 135 | [HEADERS.CONTENT_TYPE]: HEADERS.OCTET_STREAM, 136 | }, 137 | }).send(data); 138 | } 139 | 140 | public removeTarball(name: string) { 141 | return this.request({ 142 | uri: `/${encodeURIComponent(name)}/-rev/whatever`, 143 | method: 'DELETE', 144 | headers: { 145 | [HEADERS.CONTENT_TYPE]: HEADERS.JSON_CHARSET, 146 | }, 147 | }); 148 | } 149 | 150 | public removeSingleTarball(name: string, filename: string) { 151 | return this.request({ 152 | uri: `/${encodeURIComponent(name)}/-/${filename}/-rev/whatever`, 153 | method: 'DELETE', 154 | headers: { 155 | [HEADERS.CONTENT_TYPE]: HEADERS.JSON_CHARSET, 156 | }, 157 | }); 158 | } 159 | 160 | public addTag(name: string, tag: string, version: string) { 161 | return this.request({ 162 | uri: `/${encodeURIComponent(name)}/${encodeURIComponent(tag)}`, 163 | method: 'PUT', 164 | headers: { 165 | [HEADERS.CONTENT_TYPE]: HEADERS.JSON, 166 | }, 167 | }).send(JSON.stringify(version)); 168 | } 169 | 170 | public putTarballIncomplete(pkgName: string, filename: string, data: any, headerContentSize: number): Promise { 171 | const promise = this.request({ 172 | uri: `/${encodeURIComponent(pkgName)}/-/${encodeURIComponent(filename)}/whatever`, 173 | method: 'PUT', 174 | headers: { 175 | [HEADERS.CONTENT_TYPE]: HEADERS.OCTET_STREAM, 176 | [HEADERS.CONTENT_LENGTH]: headerContentSize, 177 | }, 178 | timeout: 1000, 179 | }); 180 | 181 | promise.request(function(req) { 182 | req.write(data); 183 | // it auto abort the request 184 | setTimeout(function() { 185 | req.req.abort(); 186 | }, 20); 187 | }); 188 | 189 | return new Promise(function(resolve, reject) { 190 | promise 191 | .then(function() { 192 | reject(Error('no error')); 193 | }) 194 | .catch(function(err) { 195 | if (err.code === 'ECONNRESET') { 196 | resolve(); 197 | } else { 198 | reject(err); 199 | } 200 | }); 201 | }); 202 | } 203 | 204 | public addPackage(name: string) { 205 | return this.putPackage(name, getPackage(name)) 206 | .status(HTTP_STATUS.CREATED) 207 | .body_ok(API_MESSAGE.PKG_CREATED); 208 | } 209 | 210 | public whoami() { 211 | return this.request({ 212 | uri: '/-/whoami', 213 | }) 214 | .status(HTTP_STATUS.OK) 215 | .then(function(body) { 216 | return body.username; 217 | }); 218 | } 219 | 220 | public ping() { 221 | return this.request({ 222 | uri: '/-/ping', 223 | }) 224 | .status(HTTP_STATUS.OK) 225 | .then(function(body) { 226 | return body; 227 | }); 228 | } 229 | 230 | public debug() { 231 | return this.request({ 232 | uri: '/-/_debug', 233 | method: 'GET', 234 | headers: { 235 | [HEADERS.CONTENT_TYPE]: HEADERS.JSON, 236 | }, 237 | }); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Verdaccio-GitLab 2 | 3 | Use [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) 4 | as authentication provider for the private npm registry 5 | [Verdaccio](https://github.com/verdaccio/verdaccio), the sinopia fork. 6 | 7 | [![npm](https://badge.fury.io/js/verdaccio-gitlab.svg)](http://badge.fury.io/js/verdaccio-gitlab) 8 | [![build](https://travis-ci.org/bufferoverflow/verdaccio-gitlab.svg?branch=master)](https://travis-ci.org/bufferoverflow/verdaccio-gitlab) 9 | [![dependencies](https://david-dm.org/bufferoverflow/verdaccio-gitlab/status.svg)](https://david-dm.org/bufferoverflow/verdaccio-gitlab) 10 | 11 | The main goal and differences from other sinopia/verdaccio plugins are 12 | the following: 13 | 14 | - no admin token required 15 | - user authenticates with Personal Access Token 16 | - access & publish packages depending on user rights in gitlab 17 | 18 | > This is experimental! 19 | 20 | ## Use it 21 | 22 | You need at least node version 8.x.x, codename **carbon**. 23 | 24 | ```sh 25 | git clone https://github.com/bufferoverflow/verdaccio-gitlab.git 26 | cd verdaccio-gitlab 27 | yarn install 28 | yarn start 29 | ``` 30 | 31 | > **NOTE**: Define `http_proxy` environment variable if you are behind a proxy. 32 | 33 | Verdaccio is now up and running. In order the see this plugin in action, you can 34 | use the following Verdaccio configuration in your `~/.config/verdaccio/config.yaml`. 35 | 36 | ```yaml 37 | # Verdaccio storage location relative to $HOME/.config/verdaccio 38 | storage: ./storage 39 | 40 | listen: 41 | - 0.0.0.0:4873 42 | 43 | auth: 44 | gitlab: 45 | url: https://gitlab.com 46 | 47 | uplinks: 48 | npmjs: 49 | url: https://registry.npmjs.org/ 50 | 51 | packages: 52 | '@*/*': 53 | # scoped packages 54 | access: $all 55 | publish: $maintainer 56 | proxy: npmjs 57 | gitlab: true 58 | 59 | '**': 60 | access: $all 61 | publish: $maintainer 62 | proxy: npmjs 63 | gitlab: true 64 | 65 | # Log level can be changed to info, http etc. for less verbose output 66 | logs: 67 | - {type: stdout, format: pretty, level: debug} 68 | ``` 69 | 70 | Restart Verdaccio and authenticate into it with your credentials 71 | 72 | - Username: GitLab username 73 | - Password: [Personal Access Token](https://gitlab.com/profile/personal_access_tokens) 74 | 75 | using the Web UI [http://localhost:4873](http://localhost:4873) or via npm CLI: 76 | 77 | ```sh 78 | yarn login --registry http://localhost:4873 79 | ``` 80 | 81 | and publish packages: 82 | 83 | ```sh 84 | yarn publish --registry http://localhost:4873 85 | ``` 86 | 87 | ## Access Levels 88 | 89 | Access and publish access rights are mapped following the rules below. 90 | 91 | verdaccio-gitlab access control will only be applied to package sections that 92 | are marked with `gitlab: true` as in the configuration sample above. If you 93 | wish to disable gitlab authentication to any package config, just remove the 94 | element from the config. 95 | 96 | ### Access 97 | 98 | *access* is allowed depending on the following verdaccio `package` configuration 99 | directives: 100 | 101 | - authenticated users are able to access all packages 102 | - unauthenticated users will be able to access packages marked with either 103 | `$all` or `$anonymous` access levels at the package group definition 104 | 105 | > *Please note* that no group or package name mapping is applied on access, any 106 | user successfully authenticated can access all packages. 107 | 108 | ### Publish 109 | 110 | *publish* is allowed if: 111 | 112 | 1. the package name matches the GitLab username, or 113 | 2. if the package name or scope of the package matches one of the 114 | user's GitLab groups, or 115 | 3. if the package name (possibly scoped) matches on the user's 116 | GitLab projects. 117 | 118 | For 2. and 3., the GitLab user must have the access rights on the group or 119 | project as specified in the `auth.gitlab.publish` setting. 120 | 121 | For instance, assuming the following configuration: 122 | 123 | ```yaml 124 | auth: 125 | gitlab: 126 | publish = $maintainer 127 | ``` 128 | 129 | The GitLab user `sample_user` has access to: 130 | 131 | - Group `group1` as `$maintainer` 132 | - Group `group2` as `$reporter` 133 | - Project `group3/project` as `$maintainer` 134 | 135 | Then this user would be able to: 136 | 137 | - *access* any package 138 | - *publish* any of the following packages: 139 | - `sample_user` 140 | - `group1` 141 | - any package under `@group1/**` 142 | - `@group3/project` 143 | 144 | There would be an error if the user tried to publish any package under `@group2/**`. 145 | 146 | ## Configuration Options 147 | 148 | The full set of configuration options is: 149 | 150 | ```yaml 151 | auth: 152 | gitlab: 153 | url: 154 | authCache: 155 | enabled: 156 | ttl: 157 | publish: 158 | ``` 159 | 160 | 161 | | Option | Default | Type | Description | 162 | | ------ | ------- | ---- | ----------- | 163 | | `url` | `` | url | mandatory, the url of the gitlab server | 164 | | `authCache: enabled` | `true` | boolean | activate in-memory authentication cache | 165 | | `authCache: ttl` | `300` (`0`=unlimited) | integer | time-to-live of entries in the authentication cache, in seconds | 166 | | `publish` | `$maintainer` | [`$guest`, `$reporter`, `$developer`, `$maintainer`, `$owner`] | group minimum access level of the logged in user required for npm publish operations | 167 | 168 | 169 | ## Authentication Cache 170 | 171 | In order to avoid too many authentication requests to the underlying 172 | gitlab instance, the plugin provides an in-memory cache that will save 173 | the detected groups of the users for a configurable ttl in seconds. 174 | 175 | No clear-text password is saved in-memory, just an SHA-256 hash of 176 | the user+password, plus the groups information. 177 | 178 | By default, the cache will be enabled and the credentials will be stored 179 | for 300 seconds. The ttl is checked on access, but there's also an 180 | internal timer that will check expired values regularly, so data of 181 | users not actively interacting with the system will also be eventually 182 | invalidated. 183 | 184 | > *Please note* that this implementation is in-memory and not 185 | multi-process; if the cluster module is used for starting several 186 | verdaccio processes, each process will store its own copy of the cache, 187 | so each user will actually be logged in multiple times. 188 | 189 | ## Docker 190 | 191 | ```sh 192 | git clone https://github.com/bufferoverflow/verdaccio-gitlab.git 193 | cd verdaccio-gitlab 194 | docker-compose up --build -d 195 | ``` 196 | 197 | - login with user `root` and password `verdaccio` on Gitlab via [http://localhost:50080](http://localhost:50080) 198 | - create a Personal Access Token 199 | - login to the npm registry [http://localhost:4873](http://localhost:4873) via browser 200 | - publish your packages via command line 201 | 202 | The Dockerfile provides a [default configuration file](conf/docker.yaml) 203 | that is internally available under `/verdaccio/conf/config.yaml`. In order 204 | to overwrite this configuration you can provide your own file and mount it 205 | on docker startup with the `--volume` option, or equivalent mechanism 206 | (e.g. ConfigMaps on Kubernetes / OpenShift with the 207 | [helm chart](https://github.com/helm/charts/tree/master/stable/verdaccio)). 208 | 209 | ## Development 210 | 211 | ### Contributing 212 | 213 | Please adhere to the [verdaccio community guidelines](https://github.com/verdaccio/verdaccio/blob/master/CONTRIBUTING.md) 214 | and run all the tests before creating a PR. The commit message shall follow the 215 | conventional changelog as it is enforced via local commit hook using husky and 216 | the [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint) 217 | rule set. 218 | 219 | > PR's that do not pass CI will not be reviewed. 220 | 221 | ### Create a Release 222 | 223 | Run one of the following command to create a release: 224 | 225 | ```sh 226 | yarn release:major 227 | yarn release:minor 228 | yarn release:patch 229 | ``` 230 | 231 | finally run 232 | 233 | ```sh 234 | yarn publish 235 | ``` 236 | 237 | ### Functional Tests 238 | 239 | In order to run functional tests with debug output, set the 240 | `VERDACCIO_DEBUG=true` environment variable, 241 | [as documented by verdaccio](https://github.com/verdaccio/verdaccio/wiki/Running-and-Debugging-tests): 242 | 243 | ```bash 244 | VERDACCIO_DEBUG=true yarn test:functional 245 | ``` 246 | 247 | ## License 248 | 249 | [MIT](https://spdx.org/licenses/MIT) 250 | -------------------------------------------------------------------------------- /test/unit/gitlab.spec.ts: -------------------------------------------------------------------------------- 1 | import { Callback, RemoteUser } from '@verdaccio/types'; 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import Gitlab from 'gitlab'; 4 | 5 | import { VerdaccioGitlabPackageAccess } from '../../src/gitlab'; 6 | import VerdaccioGitlab from '../../src/gitlab'; 7 | 8 | import config from './partials/config'; 9 | 10 | // Do not remove, this mocks the gitlab library 11 | 12 | describe('Gitlab Auth Plugin Unit Tests', () => { 13 | test('should create a plugin instance', () => { 14 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 15 | 16 | expect(verdaccioGitlab).toBeDefined(); 17 | }); 18 | 19 | test('should authenticate a user', done => { 20 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 21 | 22 | const cb: Callback = (err, data) => { 23 | expect(err).toBeFalsy(); 24 | expect(data.sort()).toEqual(['myGroup', 'anotherGroup/myProject', 'myUser'].sort()); 25 | done(); 26 | }; 27 | 28 | verdaccioGitlab.authenticate(config.user, config.pass, cb); 29 | }); 30 | 31 | test('should fail authentication with wrong pass', done => { 32 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 33 | const wrongPass: string = config.pass + '_wrong'; 34 | 35 | const cb: Callback = (err, data) => { 36 | expect(err).toBeTruthy(); 37 | expect(data).toBeFalsy(); 38 | done(); 39 | }; 40 | 41 | verdaccioGitlab.authenticate(config.user, wrongPass, cb); 42 | }); 43 | 44 | test('should fail authentication with non-existing user', done => { 45 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 46 | const wrongUser: string = config.user + '_wrong'; 47 | 48 | const cb: Callback = (err, data) => { 49 | expect(err).toBeTruthy(); 50 | expect(data).toBeFalsy(); 51 | done(); 52 | }; 53 | 54 | verdaccioGitlab.authenticate(wrongUser, config.pass, cb); 55 | }); 56 | 57 | test('should allow access to package based on user group', done => { 58 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 59 | const _package: VerdaccioGitlabPackageAccess = { 60 | name: '@myGroup/myPackage', 61 | access: ['$authenticated'], 62 | gitlab: true, 63 | publish: ['$authenticated'], 64 | proxy: ['npmjs'], 65 | }; 66 | 67 | const cb: Callback = (err, data) => { 68 | expect(err).toBeFalsy(); 69 | // false allows the plugin chain to continue 70 | expect(data).toBe(true); 71 | done(); 72 | }; 73 | 74 | verdaccioGitlab.allow_access(config.remoteUser, _package, cb); 75 | }); 76 | 77 | test('should allow access to package based on user project', done => { 78 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 79 | const _package: VerdaccioGitlabPackageAccess = { 80 | name: '@anotherGroup/myProject', 81 | access: ['$authenticated'], 82 | gitlab: true, 83 | publish: ['$authenticated'], 84 | proxy: ['npmjs'], 85 | }; 86 | 87 | const cb: Callback = (err, data) => { 88 | expect(err).toBeFalsy(); 89 | // false allows the plugin chain to continue 90 | expect(data).toBe(true); 91 | done(); 92 | }; 93 | 94 | verdaccioGitlab.allow_access(config.remoteUser, _package, cb); 95 | }); 96 | 97 | test('should allow access to package based on user name', done => { 98 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 99 | const _package: VerdaccioGitlabPackageAccess = { 100 | name: config.user, 101 | access: ['$authenticated'], 102 | gitlab: true, 103 | publish: ['$authenticated'], 104 | proxy: ['npmjs'], 105 | }; 106 | 107 | const cb: Callback = (err, data) => { 108 | expect(err).toBeFalsy(); 109 | // false allows the plugin chain to continue 110 | expect(data).toBe(true); 111 | done(); 112 | }; 113 | 114 | verdaccioGitlab.allow_access(config.remoteUser, _package, cb); 115 | }); 116 | 117 | test('should allow access to package when access level is empty (default = $all)', done => { 118 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 119 | const _package: VerdaccioGitlabPackageAccess = { 120 | name: config.user, 121 | access: [], 122 | gitlab: true, 123 | publish: ['$authenticated'], 124 | proxy: ['npmjs'], 125 | }; 126 | 127 | const cb: Callback = (err, data) => { 128 | expect(err).toBeFalsy(); 129 | // false allows the plugin chain to continue 130 | expect(data).toBe(true); 131 | done(); 132 | }; 133 | 134 | verdaccioGitlab.allow_access(config.remoteUser, _package, cb); 135 | }); 136 | 137 | test('should deny access to package based on unauthenticated', done => { 138 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 139 | const unauthenticatedUser: RemoteUser = { 140 | real_groups: [], 141 | groups: [], 142 | name: undefined, 143 | }; 144 | const _package: VerdaccioGitlabPackageAccess = { 145 | name: '@myGroup/myPackage', 146 | access: ['$authenticated'], 147 | gitlab: true, 148 | publish: ['$authenticated'], 149 | proxy: ['npmjs'], 150 | }; 151 | 152 | const cb: Callback = (err, data) => { 153 | expect(err).toBeTruthy(); 154 | expect(data).toBeFalsy(); 155 | done(); 156 | }; 157 | 158 | verdaccioGitlab.allow_access(unauthenticatedUser, _package, cb); 159 | }); 160 | 161 | test('should allow publish of package based on user group', done => { 162 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 163 | const _package: VerdaccioGitlabPackageAccess = { 164 | name: '@myGroup/myPackage', 165 | access: ['$all'], 166 | gitlab: true, 167 | publish: ['$authenticated'], 168 | proxy: ['npmjs'], 169 | }; 170 | 171 | const cb: Callback = (err, data) => { 172 | expect(err).toBeFalsy(); 173 | expect(data).toBe(true); 174 | done(); 175 | }; 176 | 177 | verdaccioGitlab.allow_publish(config.remoteUser, _package, cb); 178 | }); 179 | 180 | test('should allow publish of package based on user project', done => { 181 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 182 | const _package: VerdaccioGitlabPackageAccess = { 183 | name: '@anotherGroup/myProject', 184 | access: ['$all'], 185 | gitlab: true, 186 | publish: ['$authenticated'], 187 | proxy: ['npmjs'], 188 | }; 189 | 190 | const cb: Callback = (err, data) => { 191 | expect(err).toBeFalsy(); 192 | expect(data).toBe(true); 193 | done(); 194 | }; 195 | 196 | verdaccioGitlab.allow_publish(config.remoteUser, _package, cb); 197 | }); 198 | 199 | test('should allow publish of package based on user name', done => { 200 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 201 | const _package: VerdaccioGitlabPackageAccess = { 202 | name: config.user, 203 | access: ['$all'], 204 | gitlab: true, 205 | publish: ['$authenticated'], 206 | proxy: ['npmjs'], 207 | }; 208 | 209 | const cb: Callback = (err, data) => { 210 | expect(err).toBeFalsy(); 211 | expect(data).toBe(true); 212 | done(); 213 | }; 214 | 215 | verdaccioGitlab.allow_publish(config.remoteUser, _package, cb); 216 | }); 217 | 218 | test('should deny publish of package based on unauthenticated', done => { 219 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 220 | const unauthenticatedUser: RemoteUser = { 221 | real_groups: [], 222 | groups: [], 223 | name: undefined, 224 | }; 225 | const _package: VerdaccioGitlabPackageAccess = { 226 | name: config.user, 227 | access: ['$all'], 228 | gitlab: true, 229 | publish: ['$authenticated'], 230 | proxy: ['npmjs'], 231 | }; 232 | 233 | const cb: Callback = (err, data) => { 234 | expect(err).toBeTruthy(); 235 | expect(data).toBeFalsy(); 236 | done(); 237 | }; 238 | 239 | verdaccioGitlab.allow_publish(unauthenticatedUser, _package, cb); 240 | }); 241 | 242 | test('should deny publish of package based on group', done => { 243 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 244 | const _package: VerdaccioGitlabPackageAccess = { 245 | name: '@anotherGroup/myPackage', 246 | access: ['$all'], 247 | gitlab: true, 248 | publish: ['$authenticated'], 249 | proxy: ['npmjs'], 250 | }; 251 | 252 | const cb: Callback = (err, data) => { 253 | expect(err).toBeTruthy(); 254 | expect(data).toBeFalsy(); 255 | done(); 256 | }; 257 | 258 | verdaccioGitlab.allow_publish(config.remoteUser, _package, cb); 259 | }); 260 | 261 | test('should deny publish of package based on user', done => { 262 | const verdaccioGitlab: VerdaccioGitlab = new VerdaccioGitlab(config.verdaccioGitlabConfig, config.options); 263 | const _package: VerdaccioGitlabPackageAccess = { 264 | name: 'anotherUser', 265 | access: ['$all'], 266 | gitlab: true, 267 | publish: ['$authenticated'], 268 | proxy: ['npmjs'], 269 | }; 270 | 271 | const cb: Callback = (err, data) => { 272 | expect(err).toBeTruthy(); 273 | expect(data).toBeFalsy(); 274 | done(); 275 | }; 276 | 277 | verdaccioGitlab.allow_publish(config.remoteUser, _package, cb); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /src/gitlab.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Roger Meier 2 | // SPDX-License-Identifier: MIT 3 | 4 | import { Callback, IPluginAuth, Logger, PluginOptions, RemoteUser, PackageAccess } from '@verdaccio/types'; 5 | import { getInternalError, getUnauthorized, getForbidden } from '@verdaccio/commons-api'; 6 | import Gitlab from 'gitlab'; 7 | 8 | import { UserDataGroups } from './authcache'; 9 | import { AuthCache, UserData } from './authcache'; 10 | 11 | export type VerdaccioGitlabAccessLevel = '$guest' | '$reporter' | '$developer' | '$maintainer' | '$owner'; 12 | 13 | export type VerdaccioGitlabConfig = { 14 | url: string; 15 | authCache?: { 16 | enabled?: boolean; 17 | ttl?: number; 18 | }; 19 | publish?: VerdaccioGitlabAccessLevel; 20 | }; 21 | 22 | export interface VerdaccioGitlabPackageAccess extends PackageAccess { 23 | name?: string; 24 | gitlab?: boolean; 25 | } 26 | 27 | const ACCESS_LEVEL_MAPPING = { 28 | $guest: 10, 29 | $reporter: 20, 30 | $developer: 30, 31 | $maintainer: 40, 32 | $owner: 50, 33 | }; 34 | 35 | // List of verdaccio builtin levels that map to anonymous access 36 | const BUILTIN_ACCESS_LEVEL_ANONYMOUS = ['$anonymous', '$all']; 37 | 38 | // Level to apply on 'allow_access' calls when a package definition does not define one 39 | const DEFAULT_ALLOW_ACCESS_LEVEL = ['$all']; 40 | 41 | export default class VerdaccioGitLab implements IPluginAuth { 42 | private options: PluginOptions; 43 | private config: VerdaccioGitlabConfig; 44 | private authCache?: AuthCache; 45 | private logger: Logger; 46 | private publishLevel: VerdaccioGitlabAccessLevel; 47 | 48 | public constructor(config: VerdaccioGitlabConfig, options: PluginOptions) { 49 | this.logger = options.logger; 50 | this.config = config; 51 | this.options = options; 52 | this.logger.info(`[gitlab] url: ${this.config.url}`); 53 | 54 | if ((this.config.authCache || {}).enabled === false) { 55 | this.logger.info('[gitlab] auth cache disabled'); 56 | } else { 57 | const ttl = (this.config.authCache || {}).ttl || AuthCache.DEFAULT_TTL; 58 | this.authCache = new AuthCache(this.logger, ttl); 59 | this.logger.info(`[gitlab] initialized auth cache with ttl: ${ttl} seconds`); 60 | } 61 | 62 | 63 | this.publishLevel = '$maintainer'; 64 | if (this.config.publish) { 65 | this.publishLevel = this.config.publish; 66 | } 67 | 68 | if (!Object.keys(ACCESS_LEVEL_MAPPING).includes(this.publishLevel)) { 69 | throw Error(`[gitlab] invalid publish access level configuration: ${this.publishLevel}`); 70 | } 71 | this.logger.info(`[gitlab] publish control level: ${this.publishLevel}`); 72 | } 73 | 74 | public authenticate(user: string, password: string, cb: Callback) { 75 | this.logger.trace(`[gitlab] authenticate called for user: ${user}`); 76 | 77 | // Try to find the user groups in the cache 78 | const cachedUserGroups = this._getCachedUserGroups(user, password); 79 | if (cachedUserGroups) { 80 | this.logger.debug(`[gitlab] user: ${user} found in cache, authenticated with groups:`, cachedUserGroups.toString()); 81 | return cb(null, cachedUserGroups.publish); 82 | } 83 | 84 | // Not found in cache, query gitlab 85 | this.logger.trace(`[gitlab] user: ${user} not found in cache`); 86 | 87 | const GitlabAPI = new Gitlab({ 88 | url: this.config.url, 89 | token: password, 90 | }); 91 | 92 | GitlabAPI.Users.current() 93 | .then(response => { 94 | if (user.toLowerCase() !== response.username.toLowerCase()) { 95 | return cb(getUnauthorized('wrong gitlab username')); 96 | } 97 | 98 | const publishLevelId = ACCESS_LEVEL_MAPPING[this.publishLevel]; 99 | 100 | // Set the groups of an authenticated user, in normal mode: 101 | // - for access, depending on the package settings in verdaccio 102 | // - for publish, the logged in user id and all the groups they can reach as configured with access level `$auth.gitlab.publish` 103 | const gitlabPublishQueryParams = { min_access_level: publishLevelId }; 104 | 105 | this.logger.trace('[gitlab] querying gitlab user groups with params:', gitlabPublishQueryParams.toString()); 106 | 107 | const groupsPromise = GitlabAPI.Groups.all(gitlabPublishQueryParams).then(groups => { 108 | return groups.filter(group => group.path === group.full_path).map(group => group.path); 109 | }); 110 | 111 | const projectsPromise = GitlabAPI.Projects.all(gitlabPublishQueryParams).then(projects => { 112 | return projects.map(project => project.path_with_namespace); 113 | }); 114 | 115 | Promise.all([groupsPromise, projectsPromise]) 116 | .then(([groups, projectGroups]) => { 117 | const realGroups = [user, ...groups, ...projectGroups]; 118 | this._setCachedUserGroups(user, password, { publish: realGroups }); 119 | 120 | this.logger.info(`[gitlab] user: ${user} successfully authenticated`); 121 | this.logger.debug(`[gitlab] user: ${user}, with groups:`, realGroups.toString()); 122 | 123 | return cb(null, realGroups); 124 | }) 125 | .catch(error => { 126 | this.logger.error(`[gitlab] user: ${user} error querying gitlab: ${error}`); 127 | return cb(getUnauthorized('error authenticating user')); 128 | }); 129 | }) 130 | .catch(error => { 131 | this.logger.error(`[gitlab] user: ${user} error querying gitlab user data: ${error.message || {}}`); 132 | return cb(getUnauthorized('error authenticating user')); 133 | }); 134 | } 135 | 136 | public adduser(user: string, password: string, cb: Callback) { 137 | this.logger.trace(`[gitlab] adduser called for user: ${user}`); 138 | return cb(null, true); 139 | } 140 | 141 | public changePassword(user: string, password: string, newPassword: string, cb: Callback) { 142 | this.logger.trace(`[gitlab] changePassword called for user: ${user}`); 143 | return cb(getInternalError('You are using verdaccio-gitlab integration. Please change your password in gitlab')); 144 | } 145 | 146 | public allow_access(user: RemoteUser, _package: VerdaccioGitlabPackageAccess & PackageAccess, cb: Callback) { 147 | if (!_package.gitlab) return cb(null, false); 148 | 149 | const packageAccess = _package.access && _package.access.length > 0 ? _package.access : DEFAULT_ALLOW_ACCESS_LEVEL; 150 | 151 | if (user.name !== undefined) { 152 | // successfully authenticated 153 | this.logger.debug(`[gitlab] allow user: ${user.name} authenticated access to package: ${_package.name}`); 154 | return cb(null, true); 155 | } else { 156 | // unauthenticated 157 | if (BUILTIN_ACCESS_LEVEL_ANONYMOUS.some(level => packageAccess.includes(level))) { 158 | this.logger.debug(`[gitlab] allow anonymous access to package: ${_package.name}`); 159 | return cb(null, true); 160 | } else { 161 | this.logger.debug(`[gitlab] deny access to package: ${_package.name}`); 162 | return cb(getUnauthorized('access denied, user not authenticated and anonymous access disabled')); 163 | } 164 | } 165 | } 166 | 167 | public allow_publish(user: RemoteUser, _package: VerdaccioGitlabPackageAccess & PackageAccess, cb: Callback) { 168 | if (!_package.gitlab) return cb(null, false); 169 | 170 | const packageScopePermit = false; 171 | let packagePermit = false; 172 | // Only allow to publish packages when: 173 | // - the package has exactly the same name as one of the user groups, or 174 | // - the package scope is the same as one of the user groups 175 | for (const real_group of user.real_groups) { 176 | // jscs:ignore requireCamelCaseOrUpperCaseIdentifiers 177 | this.logger.trace( 178 | `[gitlab] publish: checking group: ${real_group} for user: ${user.name || ''} and package: ${_package.name}` 179 | ); 180 | 181 | if (this._matchGroupWithPackage(real_group, _package.name as string)) { 182 | packagePermit = true; 183 | break; 184 | } 185 | } 186 | 187 | if (packagePermit || packageScopePermit) { 188 | const perm = packagePermit ? 'package-name' : 'package-scope'; 189 | this.logger.debug( 190 | `[gitlab] user: ${user.name || ''} allowed to publish package: ${_package.name} based on ${perm}` 191 | ); 192 | return cb(null, true); 193 | } else { 194 | this.logger.debug(`[gitlab] user: ${user.name || ''} denied from publishing package: ${_package.name}`); 195 | // @ts-ignore 196 | const missingPerm = _package.name.indexOf('@') === 0 ? 'package-scope' : 'package-name'; 197 | return cb(getForbidden(`must have required permissions: ${this.publishLevel || ''} at ${missingPerm}`)); 198 | } 199 | } 200 | 201 | private _matchGroupWithPackage(real_group: string, package_name: string): boolean { 202 | if (real_group === package_name) { 203 | return true; 204 | } 205 | 206 | if (package_name.indexOf('@') === 0) { 207 | const split_real_group = real_group.split('/'); 208 | const split_package_name = package_name.slice(1).split('/'); 209 | 210 | if (split_real_group.length > split_package_name.length) { 211 | return false; 212 | } 213 | 214 | for (let i = 0; i < split_real_group.length; i += 1) { 215 | if (split_real_group[i] !== split_package_name[i]) { 216 | return false; 217 | } 218 | } 219 | 220 | return true; 221 | } 222 | 223 | return false; 224 | } 225 | 226 | private _getCachedUserGroups(username: string, password: string): UserDataGroups | null { 227 | if (!this.authCache) { 228 | return null; 229 | } 230 | const userData = this.authCache.findUser(username, password); 231 | return (userData || {}).groups || null; 232 | } 233 | 234 | private _setCachedUserGroups(username: string, password: string, groups: UserDataGroups): boolean { 235 | if (!this.authCache) { 236 | return false; 237 | } 238 | this.logger.debug(`[gitlab] saving data in cache for user: ${username}`); 239 | return this.authCache.storeUser(username, password, new UserData(username, groups)); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 3.0.1 (2020-01-15) 2 | 3 | ##### Chores 4 | 5 | * move babel config to package.json ([7777984e](https://github.com/bufferoverflow/verdaccio-gitlab/commit/7777984eac1bc819268f234d60187c64b9f3696d)) 6 | * require node 10 ([c2c96b60](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c2c96b607026d6243e1ff5664dfb4e7ec43c353c)) 7 | * remove unused eslint rules ([4c400380](https://github.com/bufferoverflow/verdaccio-gitlab/commit/4c400380b67978bbce327748998cb25cb7135050)) 8 | * move eslint config to package.json ([9f7fdda5](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9f7fdda5a40a9fda7a88b3f6dcf215fb6aad5cab)) 9 | * upgrade devDependencies ([bca14302](https://github.com/bufferoverflow/verdaccio-gitlab/commit/bca143026cba6bd57adbf4562be9da54094fb85a)) 10 | * remove repolinter ([708d58ba](https://github.com/bufferoverflow/verdaccio-gitlab/commit/708d58ba3236dd9ee454252d6676068c99c591b1)) 11 | * upgrade husky ([23718d8c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/23718d8c270b2c1302d0f5f736d44fa096cc9667)) 12 | * update commitlint configuration with husky ([dd8aac59](https://github.com/bufferoverflow/verdaccio-gitlab/commit/dd8aac59d7649d8ff05b4943c77a1169ccc269eb)) 13 | * use verdaccio 4 instead of 4.0 within Dockerfile ([abd814e2](https://github.com/bufferoverflow/verdaccio-gitlab/commit/abd814e213ddd6db4dc84b1e3a6886849e3d9be0)) 14 | * make license-checker less verbose ([9ced2cbf](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9ced2cbf70602e2e703317b4a7ca95c1345b0739)) 15 | * **ci:** build docker image ([1bb23656](https://github.com/bufferoverflow/verdaccio-gitlab/commit/1bb23656fbf102b14d55220f90cf3c5fa4622140)) 16 | 17 | ##### Documentation Changes 18 | 19 | * add Contributing section, cleanup ([a66fb59b](https://github.com/bufferoverflow/verdaccio-gitlab/commit/a66fb59bc461ee27f62be9773d3460f7ee0c221c)) 20 | * align please note style and use quote ([ef1163e5](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ef1163e54657f0e22643ca84d23f530361a4eeb4)) 21 | * set publish to within sample ([f03b3b4c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/f03b3b4cdce4e41a930b3011dd5bef219a1edab0)) 22 | * remove flow support as we have typescript now ([3d439af6](https://github.com/bufferoverflow/verdaccio-gitlab/commit/3d439af6d6cf2150068638fa6db03189d7404029)) 23 | * clarify publish options ([2dfde28a](https://github.com/bufferoverflow/verdaccio-gitlab/commit/2dfde28a8e5b767f373ef490ce232dc6aeb72176)) 24 | 25 | ##### Refactors 26 | 27 | * set authCache as optional member ([a9e12460](https://github.com/bufferoverflow/verdaccio-gitlab/commit/a9e124604b2673a4b3155551e0e133a48110e0a9)) 28 | 29 | ##### Code Style Changes 30 | 31 | * use typescript style import ([97685933](https://github.com/bufferoverflow/verdaccio-gitlab/commit/97685933765a25d280b32eda3238891e65d537ac)) 32 | * set member accessibility explicit ([21afb3d7](https://github.com/bufferoverflow/verdaccio-gitlab/commit/21afb3d74b5fe8b20eccc60440f9fbfb231d5fc0)) 33 | * remove useless ts-ignore, fix no-use-before-define ([23fcd472](https://github.com/bufferoverflow/verdaccio-gitlab/commit/23fcd472216aa96234325960c8027b4444d0a218)) 34 | * adopt code style to rules ([f30d3821](https://github.com/bufferoverflow/verdaccio-gitlab/commit/f30d3821928d3ba91af3d26a56c197bdfb675884)) 35 | 36 | ##### Tests 37 | 38 | * align functional test suite to verdaccio ([fd2b97b4](https://github.com/bufferoverflow/verdaccio-gitlab/commit/fd2b97b4d02e066da20c2329801a78daa505a7f6)) 39 | * convert package fixture to typescript ([d3374af8](https://github.com/bufferoverflow/verdaccio-gitlab/commit/d3374af870e81fd1bc1bf2152be1cd7542247480)) 40 | 41 | ## 3.0.0 (2019-12-19) 42 | 43 | ##### Chores 44 | 45 | * **lint:** add markdownlint, use lint instead of lint:ts witin ci ([8af48c38](https://github.com/bufferoverflow/verdaccio-gitlab/commit/8af48c3877c9ea8d8b0d718d2aa1041ca7f9a70f)) 46 | * upgrade commitlint and jest-environment-node ([6d4fd063](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6d4fd0630daf1dc5e3623b4d55926add265e6f4a)) 47 | * remove warning ([74e9e8e9](https://github.com/bufferoverflow/verdaccio-gitlab/commit/74e9e8e90363b201c87d413a60bf2d0b97fdb3be)) 48 | * add prettier conf ([5dc2c674](https://github.com/bufferoverflow/verdaccio-gitlab/commit/5dc2c674a1866c79fb1738d2eb470999031c936c)) 49 | * remove eclint ([2f04b5cd](https://github.com/bufferoverflow/verdaccio-gitlab/commit/2f04b5cd935323f75eedeb114b96393a02f5adf9)) 50 | * fix wrong error ([f7736b46](https://github.com/bufferoverflow/verdaccio-gitlab/commit/f7736b46e75970bfb6a133c3abb8ab5565e0beef)) 51 | * add missing dependency ([8407cdac](https://github.com/bufferoverflow/verdaccio-gitlab/commit/8407cdac9b05d04358c910d712e00099aceeec88)) 52 | * fix test ([9a424f9d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9a424f9d903c121ee2b2a4e28382ba056d19f422)) 53 | * fix lint issues ([4eed60d7](https://github.com/bufferoverflow/verdaccio-gitlab/commit/4eed60d764d450f731be87bc470449347ff6c30b)) 54 | * lint issues ([6085b6fc](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6085b6fc7efb79aba619f3deed2b804a4f57bf1a)) 55 | * migrating to typescript ([56d11877](https://github.com/bufferoverflow/verdaccio-gitlab/commit/56d118775f4ccb06f074015e9eddf993cece4428)) 56 | * upgrade verdaccio to latest stable 4.0.0 ([ebdea8ac](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ebdea8accc58952486909120ce78da8f9e4b8ae7)) 57 | * **deps:** 58 | * bump handlebars from 4.0.11 to 4.1.2 ([c3acd112](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c3acd112a087361028bd1534805d719464cdfe6f)) 59 | * allow patches & minor updates on verdaccio dependency ([6d752fee](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6d752fee1fc5c37fe10f687304bd49b56d12a7b1)) 60 | * **deps-dev:** bump lodash from 4.17.10 to 4.17.13 ([82848808](https://github.com/bufferoverflow/verdaccio-gitlab/commit/82848808f5156c09b12a765cda998847dff1d8cf)) 61 | * **docker:** update verdaccio dep to 4.0 ([c2beeba0](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c2beeba096a7c802a3fd49889521e7e1b10e60c4)) 62 | 63 | ##### Documentation Changes 64 | 65 | * remove references to normal mode in readme ([ab36d309](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ab36d309d45a6b7a49c606c53d54336eb8c4a46a)) 66 | * fix typo within README.md ([23c8a598](https://github.com/bufferoverflow/verdaccio-gitlab/commit/23c8a598352d16a87b8e40e107b1969b2added4e)) 67 | 68 | ##### New Features 69 | 70 | * get rid of legacy mode ([2fa6944c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/2fa6944cc3f43c31f32127a61bb4e09b22504461)) 71 | 72 | ##### Bug Fixes 73 | 74 | * **auth:** 75 | * compare to lower-cased version of user ([82b7d1a8](https://github.com/bufferoverflow/verdaccio-gitlab/commit/82b7d1a85760b7780e331240d9bd1e6e0bc3388c)) 76 | * allows matching of mixed-case usernames ([d75f3003](https://github.com/bufferoverflow/verdaccio-gitlab/commit/d75f30030a390de7597a18aa284779e4c508ba70)) 77 | 78 | ##### Refactors 79 | 80 | * update dependencies and script ([87c679d3](https://github.com/bufferoverflow/verdaccio-gitlab/commit/87c679d3fe1e2650165a2ed3876feaa90adf71a3)) 81 | * update dependencies ([cde1a60f](https://github.com/bufferoverflow/verdaccio-gitlab/commit/cde1a60f021c4e809c88e011e91a3d19bbfe1596)) 82 | * upgrade to latest flow-types for verdaccio 4.x ([03db18d9](https://github.com/bufferoverflow/verdaccio-gitlab/commit/03db18d973425338599fe8b2cdbbbf871868ba44)) 83 | 84 | ### 2.2.0 (2018-12-07) 85 | 86 | ##### New Features 87 | 88 | * add project groups ([cb13b62c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/cb13b62c0adef627cbc00f227348e11a20dad9ac)) 89 | 90 | ### 2.1.0 (2018-11-23) 91 | 92 | ##### Chores 93 | 94 | * upgrade verdaccio to latest stable 3.8.6 ([ee3f3d97](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ee3f3d97daf6116bd7761fc4d59e75b8cdb833c5)) 95 | * add functional tests ([22e91e81](https://github.com/bufferoverflow/verdaccio-gitlab/commit/22e91e81ceb32e3a0df5b06165e0cc4a9a86194f)) 96 | * add functional tests ([6f2d6b62](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6f2d6b625ecc6dfcaa9852685c5c2ac971f107a0)) 97 | 98 | ##### Documentation Changes 99 | 100 | * explain how to overwrite default conf with dockerfile ([76ad223c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/76ad223cee4c49a8785d95beb4b1337b48da7441)) 101 | 102 | ##### New Features 103 | 104 | * make allow_access behave closer to htpasswd default auth plugin ([ccf53254](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ccf53254d95555a9232ad617aa680b7cc2e883dc)) 105 | * make allow_access behave closer to htpasswd default auth plugin ([6e4c1768](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6e4c1768428438e0199f25ed3c4b568e877527a6)) 106 | * make allow_access behave closer to htpasswd default auth plugin ([366bc5c2](https://github.com/bufferoverflow/verdaccio-gitlab/commit/366bc5c21e512ad717064701314277b109b0697c)) 107 | 108 | ##### Refactors 109 | 110 | * consistent use of plugin chain ([14b8b186](https://github.com/bufferoverflow/verdaccio-gitlab/commit/14b8b1860593c818fa14825fe7be05f89929883f)) 111 | 112 | ## 2.0.0 (2018-09-03) 113 | 114 | ##### Chores 115 | 116 | * fix yarn parameter for publishing ([adfee8e4](https://github.com/bufferoverflow/verdaccio-gitlab/commit/adfee8e4f8851646a903edfdf2e4c0a65843a419)) 117 | * misc corrections ([d81439d4](https://github.com/bufferoverflow/verdaccio-gitlab/commit/d81439d41f965d01e4fbedb5ad288c3b95ede43d)) 118 | * misc corrections ([37c46e69](https://github.com/bufferoverflow/verdaccio-gitlab/commit/37c46e69c20b30b105fa41fcc70d932dbbe857a8)) 119 | * automated unit testing with jest ([b506d7ae](https://github.com/bufferoverflow/verdaccio-gitlab/commit/b506d7ae2395244594187774b88274504c780335)) 120 | * build refactor and jest testing support ([1e515c0d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/1e515c0d5547b2c80ea9a324f93c814d867c85dd)) 121 | * update dependencies ([c2567be4](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c2567be4baa5acd9bfa16999968cb329865864b9)) 122 | * fix yarn scripts syntax ([c11aad20](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c11aad2010d0859e23f0df8c40da4240c5839900)) 123 | * improve logging statements ([0462e349](https://github.com/bufferoverflow/verdaccio-gitlab/commit/0462e34995841eb5e383d5a49830fc39fe5737f9)) 124 | * update all dependencies ([48b9a729](https://github.com/bufferoverflow/verdaccio-gitlab/commit/48b9a729cb43eb92530b356022fd062eb8048468)) 125 | * drop node 6 support ([144cdb46](https://github.com/bufferoverflow/verdaccio-gitlab/commit/144cdb46293e2ad608db94f8878c5a7240ac24f1)) 126 | * fix node runtime dependencies ([a616c253](https://github.com/bufferoverflow/verdaccio-gitlab/commit/a616c25373d10112b319ed3854aa216eef64d450)) 127 | * used node >= 6.12.0 ([70b98348](https://github.com/bufferoverflow/verdaccio-gitlab/commit/70b983482a1ad6d81a9771e08165be8fca91422d)) 128 | * get rid of npm linka and run verdaccio cli ([aeb16899](https://github.com/bufferoverflow/verdaccio-gitlab/commit/aeb168993e97530b8748199bf363b2416face7a0)) 129 | * update devDependencies ([8520b471](https://github.com/bufferoverflow/verdaccio-gitlab/commit/8520b4719bf803cb9d8546fd242d2a7ccae92ef6)) 130 | * use gitlab 3.2.2 ([360959aa](https://github.com/bufferoverflow/verdaccio-gitlab/commit/360959aab07c260ebf30916e5741b3a583dc76d5)) 131 | * use verdaccio v3.0.0-beta.7 ([e4f66624](https://github.com/bufferoverflow/verdaccio-gitlab/commit/e4f6662416691a05fd0aa2f81e9f8b8b77e5b048)) 132 | * **lint:** 133 | * upgrade markdownlint-cli ([20bb5a59](https://github.com/bufferoverflow/verdaccio-gitlab/commit/20bb5a59fa42fa5ed6d31c29dc4538cf95699bef)) 134 | * use eslint instead of jslint and jshint ([82bf88ea](https://github.com/bufferoverflow/verdaccio-gitlab/commit/82bf88eaa0f8a5711f3989cd6a68f57d5d13738f)) 135 | 136 | ##### Documentation Changes 137 | 138 | * mention that at least node 8 (carbon) is required ([0521b4b1](https://github.com/bufferoverflow/verdaccio-gitlab/commit/0521b4b12283f91fef011d49dd0188c694f6b8f0)) 139 | * use alt text for badges ([27296ee3](https://github.com/bufferoverflow/verdaccio-gitlab/commit/27296ee34628f81c0629705b7a2832ed1744ef23)) 140 | * use GitHub issues instead of Todo within README.md ([ca061e90](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ca061e904fcf35ec86b1fec7c2942873f87ac3f2)) 141 | * **readme:** 142 | * minor typo and grammatical fixes to readme ([08b0276b](https://github.com/bufferoverflow/verdaccio-gitlab/commit/08b0276b6966b4cf8975af253c6efcbba98a0e49)) 143 | * add usage clarifications in readme ([71ff969c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/71ff969c44578a432e6ad2125bb8a758417c5b35)) 144 | 145 | ##### New Features 146 | 147 | * gitlab-11.2-group-api-improvements ([207a490d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/207a490dc4d9da4784c69c0f5d428e08d96e01bc)) 148 | * gitlab-11.2-group-api-improvements ([68068bf1](https://github.com/bufferoverflow/verdaccio-gitlab/commit/68068bf1c96c4c82805d39d13d71e2b9c615e02f)) 149 | * gitlab-11.2-group-api-improvements ([9353fc37](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9353fc37d5664e08b1e7e1e1eed463e787d1ad4e)) 150 | * use types and classes ([255f85b5](https://github.com/bufferoverflow/verdaccio-gitlab/commit/255f85b547cd1e3b64dfb5908f890d6211a589d6)) 151 | * transpile using babel ([68659626](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6865962628f9e006c1bf5fc71eff7e7b64488511)) 152 | 153 | ##### Bug Fixes 154 | 155 | * improve auth handling ([8bde1977](https://github.com/bufferoverflow/verdaccio-gitlab/commit/8bde1977fde91d75ae7103a40ce9c4bf093e1534)) 156 | * flow ([f4113963](https://github.com/bufferoverflow/verdaccio-gitlab/commit/f4113963cfd0d0c836db08d128092b21d475e300)) 157 | 158 | ##### Refactors 159 | 160 | * access should fail if unauthenticated depending on verdaccio ([8310d05c](https://github.com/bufferoverflow/verdaccio-gitlab/commit/8310d05c4977a28fa8e76df763bb775324db94d6)) 161 | * docker build improvements ([04603ad1](https://github.com/bufferoverflow/verdaccio-gitlab/commit/04603ad12c17b0f4f2c6d20d9af89d38af60f0f6)) 162 | * migrate from npm to yarn ([35a555e9](https://github.com/bufferoverflow/verdaccio-gitlab/commit/35a555e96cac3c334a8258efd48b2b2c50b352e3)) 163 | * add flow support to eslint configuration ([0f3d4a44](https://github.com/bufferoverflow/verdaccio-gitlab/commit/0f3d4a44be43a359766e267f213414e391c900bd)) 164 | * add configurable authentication cache ([31bb3096](https://github.com/bufferoverflow/verdaccio-gitlab/commit/31bb309638f0240f97da084d07da7f462311832e)) 165 | * **gitlab:** simplify allow_publish function ([dbe36e3d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/dbe36e3dabf4a2ee72b8daacabd22c8daeed63ee)) 166 | * **dockerfile:** multistage builder / random uid user support ([6edd7016](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6edd701682719582a03fbba8b808785859a1f08b)) 167 | 168 | ##### Code Style Changes 169 | 170 | * **gitlab:** consistent white-space usage ([eea4c9ec](https://github.com/bufferoverflow/verdaccio-gitlab/commit/eea4c9ece3170de3511c1631c588d2cee5d40eb8)) 171 | 172 | ## 1.0.0 (2018-02-02) 173 | 174 | ##### Bug Fixes 175 | 176 | * call next plugin ([e4aaa339](https://github.com/bufferoverflow/verdaccio-gitlab/commit/e4aaa339c7ead89a381e59a2be4201980ffc0951)) 177 | * **docker:** remove registry stuff from docker-compose ([6cd53c7e](https://github.com/bufferoverflow/verdaccio-gitlab/commit/6cd53c7ed49dba3cfa6f6100b3796ece304b5cfb)) 178 | 179 | #### 0.0.5 (2018-01-22) 180 | 181 | ##### Chores 182 | 183 | * use husky and git commitmsg hook ([eb23c9f5](https://github.com/bufferoverflow/verdaccio-gitlab/commit/eb23c9f52c48537178b2fc9caf9dc44d449b18b8)) 184 | * use node-gitlab-api 2.2.0 ([c7b682dc](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c7b682dcfc3e55a3fba9827a82cf54fa86abe97d)) 185 | * use license-checker to enforce license compliance ([c8008d4b](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c8008d4b2d16b3300af8a22ce662e92983ce9b61)) 186 | * use verdaccio 2.7.3 ([01350e61](https://github.com/bufferoverflow/verdaccio-gitlab/commit/01350e610e3557c96b1e10e59dd2fc66ee5475be)) 187 | * **docker:** use carbon-alpine ([72b84e40](https://github.com/bufferoverflow/verdaccio-gitlab/commit/72b84e40c100e404f9feb8792b04eb327e9b0c4f)) 188 | 189 | ##### Documentation Changes 190 | 191 | * add development section ([a0561b75](https://github.com/bufferoverflow/verdaccio-gitlab/commit/a0561b75acdfcfba12d502f776e676e4988ca768)) 192 | 193 | ##### New Features 194 | 195 | * authorize access for authenticated users ([58eb2cd3](https://github.com/bufferoverflow/verdaccio-gitlab/commit/58eb2cd36079f42b0c4d43340727285173d0895e)) 196 | 197 | #### 0.0.4 (2018-01-14) 198 | 199 | ##### Documentation Changes 200 | 201 | * improve authorization related topics ([d1f06445](https://github.com/bufferoverflow/verdaccio-gitlab/commit/d1f0644537cd1a06ea8c4098f05a30d5daf8741f)) 202 | 203 | ##### New Features 204 | 205 | * changelog generator ([47653197](https://github.com/bufferoverflow/verdaccio-gitlab/commit/476531977e082148068ac806526612ff5498be07)) 206 | 207 | #### 0.0.3 (2018-01-14) 208 | 209 | ##### Chores 210 | 211 | * use commitlint/travis-cli and lts versions within travis ([5a2a0039](https://github.com/bufferoverflow/verdaccio-gitlab/commit/5a2a0039f8661aed2162ec4273aa273b1d8473dd)) 212 | * update package-lock.json ([9801b215](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9801b215f0b86d480bf148cd10e90993d44789e6)) 213 | * update commitlint ([ea3285b8](https://github.com/bufferoverflow/verdaccio-gitlab/commit/ea3285b870656074804546bdea1b434cc1df976f)) 214 | * update description and tags ([c30957e9](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c30957e931c58345db569d91b2421baae39bfb68)) 215 | * commitlint as part of lint ([7823617d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/7823617d24a5ac35562e0e5a20e73b83f2709922)) 216 | * add commitlint/config-conventional config ([11f7d18f](https://github.com/bufferoverflow/verdaccio-gitlab/commit/11f7d18f6c13fe83249e36876c2866f21b955f0d)) 217 | 218 | ##### Documentation Changes 219 | 220 | * add fury, travis-ci and david-dm badge ([c1417fb7](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c1417fb7539e8014485af5efef12e39219cf7168)) 221 | * update todo section within README ([783e38ba](https://github.com/bufferoverflow/verdaccio-gitlab/commit/783e38ba83cd838f2638aa446aee3f3c7a964b83)) 222 | 223 | ##### New Features 224 | 225 | * authorize publish based on group ownership ([9877bcf1](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9877bcf15967c3c21b1b42d2758cae36f2a9e5af)) 226 | * docker compose setup ([e92c729a](https://github.com/bufferoverflow/verdaccio-gitlab/commit/e92c729ab50b4d0dd006a203332735c34b6b47db)) 227 | 228 | #### 0.0.2 (2018-01-11) 229 | 230 | ##### Chores 231 | 232 | * vscode is great to debug ([9b3a167d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/9b3a167dce88b09d88a50ae6983862c59762bdba)) 233 | 234 | ##### Documentation Changes 235 | 236 | * use verdaccio github url ([83e04a08](https://github.com/bufferoverflow/verdaccio-gitlab/commit/83e04a08a6a8a9dc98684524d13f0195926528fc)) 237 | 238 | ##### Bug Fixes 239 | 240 | * add user name to groups ([61bda536](https://github.com/bufferoverflow/verdaccio-gitlab/commit/61bda5360456c884acf3f6c38f30a46e852dc455)) 241 | 242 | #### 0.0.1 (2018-01-07) 243 | 244 | ##### Chores 245 | 246 | * simplify use it ([dec7e56a](https://github.com/bufferoverflow/verdaccio-gitlab/commit/dec7e56a8a90b8ef6c113716d669a7bfb6b5bc54)) 247 | * update package.json, .travis.yml ([980156f9](https://github.com/bufferoverflow/verdaccio-gitlab/commit/980156f932ad2e67418c3c3d3b1747e7bd948e16)) 248 | 249 | ##### Documentation Changes 250 | 251 | * improve use it ([c870ccc8](https://github.com/bufferoverflow/verdaccio-gitlab/commit/c870ccc8c34e68ee77218ce1192d346bd539a251)) 252 | * use it section ([dece91b8](https://github.com/bufferoverflow/verdaccio-gitlab/commit/dece91b8d82701288e47dcfc6474845f1d0277dc)) 253 | * gitlab and verdaccio infos ([70cf3963](https://github.com/bufferoverflow/verdaccio-gitlab/commit/70cf3963f14d06c04b3c4550a69f853baaff2a86)) 254 | * add inspired by ([70d44708](https://github.com/bufferoverflow/verdaccio-gitlab/commit/70d4470850a2ff695d4d548cf8f670eaa9af9076)) 255 | 256 | ##### New Features 257 | 258 | * use node-gitlab-api and populate owned groups ([1b51b125](https://github.com/bufferoverflow/verdaccio-gitlab/commit/1b51b125ba62de64a3aaf7a303513d74af9470df)) 259 | * travis-ci ([e67b4e9d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/e67b4e9d67d14aeed1ef4288135e63e96b131ac6)) 260 | 261 | ##### Bug Fixes 262 | 263 | * npm start by using npm link ([65e8f243](https://github.com/bufferoverflow/verdaccio-gitlab/commit/65e8f24350ae86999b62f6582337ad6086e94718)) 264 | * adduser is not supported ([8c8a678d](https://github.com/bufferoverflow/verdaccio-gitlab/commit/8c8a678dbfac1958468186d113dc0bb2207a56fb)) 265 | --------------------------------------------------------------------------------