Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce group and project search strategies #144

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:12.16.3-alpine3.11 as builder
FROM node:14 as builder

WORKDIR /opt/verdaccio-gitlab-build
COPY . .
Expand All @@ -14,7 +14,7 @@ RUN yarn config set registry $VERDACCIO_BUILD_REGISTRY && \



FROM verdaccio/verdaccio:4
FROM verdaccio/verdaccio:5
LABEL maintainer="https://github.com/bufferoverflow/verdaccio-gitlab"

# Go back to root to be able to install the plugin
Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ the following:

## Use it

You need at least node version 8.x.x, codename **carbon**.
You need at least node version 12.18.x, codename **carbon**.

```sh
git clone https://github.com/bufferoverflow/verdaccio-gitlab.git
Expand Down Expand Up @@ -107,7 +107,9 @@ user successfully authenticated can access all packages.

### Publish

*publish* is allowed if:
We have two matching strategies between the package name and the names of GitLab groups and projects, the 'default' and 'nameMapping' strategies.

<b>*publish* with "default" strategy is allowed if</b>:

1. the package name matches the GitLab username, or
2. if the package name or scope of the package matches one of the
Expand Down Expand Up @@ -143,6 +145,14 @@ Then this user would be able to:

There would be an error if the user tried to publish any package under `@group2/**`.

<b>*publish* with "nameMapping" strategy</b>:

The same rules apply, but with some differences:
* In cases where the scope or project name in the package is different from the group or project in GitLab, it is possible to define the mapping for these names.
* Use case-insensitive matching.
* Define the search path for group names through the searchPath variable, following the GroupSchema from the GitLab API.
* Define the search path for project names through the searchPath variable, following the ProjectSchema from the GitLab API.

## Configuration Options

The full set of configuration options is:
Expand All @@ -155,6 +165,19 @@ auth:
enabled: <boolean>
ttl: <integer>
publish: <string>
groupSearchStrategy: 'default|nameMapping'
groupsStrategy: # only available on groupSearchStrategy 'nameMapping'
caseSensitive: <boolean>
searchPath: <string>
mappings:
- gitlabName: <string>
packageJsonName: <string>
projectsStrategy: # only available on groupSearchStrategy 'nameMapping'
caseSensitive: <boolean>
searchPath: <string>
mappings:
- gitlabName: <string>
packageJsonName: <string>
```

<!-- markdownlint-disable MD013 -->
Expand All @@ -164,6 +187,9 @@ auth:
| `authCache: enabled` | `true` | boolean | activate in-memory authentication cache |
| `authCache: ttl` | `300` (`0`=unlimited) | integer | time-to-live of entries in the authentication cache, in seconds |
| `publish` | `$maintainer` | [`$guest`, `$reporter`, `$developer`, `$maintainer`, `$owner`] | group minimum access level of the logged in user required for npm publish operations |
| `groupSearchStrategy` | `'default'` | ['default', 'nameMapping'] | strategy for searching GitLab groups |
| `groupsStrategy` | | Like yaml structure above | configuration for group search strategy (only available on `groupSearchStrategy 'nameMapping'`) |
| `projectsStrategy` | | Like yaml structure above | configuration for project search strategy (only available on `groupSearchStrategy 'nameMapping'`) |
<!-- markdownlint-enable MD013 -->

## Authentication Cache
Expand Down
5 changes: 2 additions & 3 deletions conf/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@ packages:
proxy: npmjs
gitlab: true

logs:
- { type: stdout, format: pretty, level: info }
#- {type: file, path: verdaccio.log, level: info}
log: { type: stdout, format: pretty, level: info }
# {type: file, path: verdaccio.log, level: info}
16 changes: 11 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"name": "Roger Meier",
"email": "[email protected]"
},
"contributors": [
{
"name": "Josué Freitas",
"email": "[email protected]"
}
],
"scripts": {
"type-check": "tsc --noEmit",
"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",
Expand All @@ -25,7 +31,7 @@
"test:all": "yarn test && yarn test:functional"
},
"main": "build/index.js",
"version": "3.0.1",
"version": "3.1.0",
"description": "private npm registry (Verdaccio) using gitlab-ce as authentication and authorization provider",
"keywords": [
"sinopia",
Expand All @@ -46,14 +52,14 @@
"url": "https://github.com/bufferoverflow/verdaccio-gitlab/issues"
},
"engines": {
"node": ">=10"
"node": ">=12.18"
},
"dependencies": {
"gitlab": "3.5.1",
"global-tunnel-ng": "2.5.3",
"gitlab": "14.2.2",
"global-tunnel-ng": "2.7.1",
"http-errors": "1.7.3",
"node-cache": "4.2.0",
"verdaccio": "^4.3.4"
"verdaccio": "^5.29.0"
},
"devDependencies": {
"@commitlint/cli": "^8.3.3",
Expand Down
52 changes: 34 additions & 18 deletions src/gitlab.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
// Copyright 2018 Roger Meier <[email protected]>
// Version
// SPDX-License-Identifier: MIT

import { Callback, IPluginAuth, Logger, PluginOptions, RemoteUser, PackageAccess } from '@verdaccio/types';
import { getInternalError, getUnauthorized, getForbidden } from '@verdaccio/commons-api';
import Gitlab from 'gitlab';
import { Gitlab } from 'gitlab';

import groupSearchStrategies from './groupSearchStrategies';
import { UserDataGroups } from './authcache';
import { AuthCache, UserData } from './authcache';

export type VerdaccioGitlabAccessLevel = '$guest' | '$reporter' | '$developer' | '$maintainer' | '$owner';

export type VerdaccioGitlabNameMapping = {
gitlabName: string,
packageJsonName: string
};

export type VerdaccioGitLabStrategy = {
caseSensitive?: boolean,
searchPath?: string,
mappings?: VerdaccioGitlabNameMapping[]
};

export type VerdaccioGitlabConfig = {
url: string;
authCache?: {
enabled?: boolean;
ttl?: number;
};
publish?: VerdaccioGitlabAccessLevel;
groupsStrategy?: VerdaccioGitLabStrategy; // only available on groupSearchStrategy 'nameMapping'
projectsStrategy?: VerdaccioGitLabStrategy; // only available on groupSearchStrategy 'nameMapping'
groupSearchStrategy?: 'default' | 'nameMapping';
};

export interface VerdaccioGitlabPackageAccess extends PackageAccess {
Expand Down Expand Up @@ -59,7 +75,6 @@ export default class VerdaccioGitLab implements IPluginAuth<VerdaccioGitlabConfi
this.logger.info(`[gitlab] initialized auth cache with ttl: ${ttl} seconds`);
}


this.publishLevel = '$maintainer';
if (this.config.publish) {
this.publishLevel = this.config.publish;
Expand All @@ -85,13 +100,13 @@ export default class VerdaccioGitLab implements IPluginAuth<VerdaccioGitlabConfi
this.logger.trace(`[gitlab] user: ${user} not found in cache`);

const GitlabAPI = new Gitlab({
url: this.config.url,
host: this.config.url,
token: password,
});

GitlabAPI.Users.current()
.then(response => {
if (user.toLowerCase() !== response.username.toLowerCase()) {
if (user.toLowerCase() !== response['username']?.toLowerCase()) {
return cb(getUnauthorized('wrong gitlab username'));
}

Expand All @@ -104,12 +119,23 @@ export default class VerdaccioGitLab implements IPluginAuth<VerdaccioGitlabConfi

this.logger.trace('[gitlab] querying gitlab user groups with params:', gitlabPublishQueryParams.toString());

const groupSearchPath = this.config.groupsStrategy?.searchPath ?? 'path';
const projectSearchPath = this.config.projectsStrategy?.searchPath ?? 'path_with_namespace';

const groupsPromise = GitlabAPI.Groups.all(gitlabPublishQueryParams).then(groups => {
return groups.filter(group => group.path === group.full_path).map(group => group.path);
return groups.filter(group => group.path === group.full_path || this.config.groupsStrategy).map(group =>
this.config.groupsStrategy?.caseSensitive
? group[groupSearchPath]?.toLowerCase() ?? ''
: group[groupSearchPath] ?? ''
);
});

const projectsPromise = GitlabAPI.Projects.all(gitlabPublishQueryParams).then(projects => {
return projects.map(project => project.path_with_namespace);
return projects.map(project =>
this.config.projectsStrategy?.caseSensitive
? project[projectSearchPath].toLowerCase() ?? ''
: project[projectSearchPath] ?? ''
);
});

Promise.all([groupsPromise, projectsPromise])
Expand Down Expand Up @@ -174,7 +200,7 @@ export default class VerdaccioGitLab implements IPluginAuth<VerdaccioGitlabConfi
// - the package scope is the same as one of the user groups
for (const real_group of user.real_groups) {
// jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
this.logger.trace(
this.logger.info(
`[gitlab] publish: checking group: ${real_group} for user: ${user.name || ''} and package: ${_package.name}`
);

Expand Down Expand Up @@ -207,17 +233,7 @@ export default class VerdaccioGitLab implements IPluginAuth<VerdaccioGitlabConfi
const split_real_group = real_group.split('/');
const split_package_name = package_name.slice(1).split('/');

if (split_real_group.length > split_package_name.length) {
return false;
}

for (let i = 0; i < split_real_group.length; i += 1) {
if (split_real_group[i] !== split_package_name[i]) {
return false;
}
}

return true;
return groupSearchStrategies[this.config.groupSearchStrategy ?? 'default'](split_real_group, split_package_name, this.config);
}

return false;
Expand Down
74 changes: 74 additions & 0 deletions src/groupSearchStrategies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { VerdaccioGitlabConfig } from "./gitlab";

function defaultStrategy(split_real_group: string[], split_package_name: string[]): boolean {
if (split_real_group.length > split_package_name.length) {
return false;
}

for (let i = 0; i < split_real_group.length; i += 1) {
if (split_real_group[i] !== split_package_name[i]) {
return false;
}
}

return true;
}

function nameMappingStrategy(splitRealGroup: string[], splitPackageName: string[], config: VerdaccioGitlabConfig): boolean {
if (splitPackageName?.length === 0 || splitRealGroup?.length === 0) {
return false;
}

const groupsMapping = config.groupsStrategy?.mappings;
const projectsMapping = config.projectsStrategy?.mappings;

const treatLowerCase = (source: string, caseSensitive: boolean) => (
caseSensitive && source
? source.toLowerCase()
: source
);

// We just need the last one, because there will be the real group or project name
const realGroupOrProjectName = splitRealGroup[splitRealGroup.length - 1];

for (const packageNamePart of splitPackageName) {
let caseSensitive = config.groupsStrategy?.caseSensitive ?? config.projectsStrategy?.caseSensitive ?? false;

if (treatLowerCase(packageNamePart, caseSensitive) === treatLowerCase(realGroupOrProjectName, caseSensitive)) {
return true;
}

caseSensitive = config.groupsStrategy?.caseSensitive ?? false;

const matchedGroup = groupsMapping?.find(groupMapping =>
treatLowerCase(groupMapping.gitlabName, caseSensitive) === treatLowerCase(realGroupOrProjectName, caseSensitive)
);

if (matchedGroup && splitPackageName.length > 1 &&
treatLowerCase(packageNamePart, caseSensitive) ===
treatLowerCase(matchedGroup.packageJsonName, caseSensitive)
) {
return true;
}

caseSensitive = config.projectsStrategy?.caseSensitive ?? false;

const matchedPackageJsonName = projectsMapping?.find(project =>
treatLowerCase(project.packageJsonName, caseSensitive) === treatLowerCase(packageNamePart, caseSensitive)
);

if (matchedPackageJsonName &&
treatLowerCase(matchedPackageJsonName.gitlabName, caseSensitive) ===
treatLowerCase(realGroupOrProjectName, caseSensitive)
) {
return true;
}
}

return false;
}

export default {
default: defaultStrategy,
nameMapping: nameMappingStrategy
}
Loading