2
2
// SPDX-License-Identifier: MIT
3
3
// @flow
4
4
5
+ import type { Callback , IPluginAuth , Logger , PluginOptions , RemoteUser , PackageAccess } from '@verdaccio/types' ;
6
+ import type { UserDataGroups } from './authcache' ;
7
+
5
8
import Gitlab from 'gitlab' ;
6
9
import { AuthCache , UserData } from './authcache' ;
7
10
import httperror from 'http-errors' ;
8
- import type { Callback , Config , IPluginAuth , Logger , PluginOptions , RemoteUser , PackageAccess } from '@verdaccio/types' ;
9
11
10
- export type VerdaccioGitlabConfig = Config & {
12
+ export type VerdaccioGitlabAccessLevel =
13
+ '$guest' |
14
+ '$reporter' |
15
+ '$developer' |
16
+ '$maintainer' |
17
+ '$owner' ;
18
+
19
+ export type VerdaccioGitlabConfig = {
11
20
url : string ,
12
21
authCache ?: {
13
22
enabled ?: boolean ,
14
23
ttl ?: number
15
- }
24
+ } ,
25
+ access ?: VerdaccioGitlabAccessLevel ,
26
+ publish ?: VerdaccioGitlabAccessLevel
16
27
} ;
17
28
18
- type VerdaccioGitlabPackageAccess = PackageAccess & {
29
+ export type VerdaccioGitlabPackageAccess = PackageAccess & {
19
30
name : string ,
20
31
gitlab ?: boolean
21
32
}
22
33
34
+ const ACCESS_LEVEL_MAPPING = {
35
+ $guest : 10 ,
36
+ $reporter : 20 ,
37
+ $developer : 30 ,
38
+ $maintainer : 40 ,
39
+ $owner : 50
40
+ } ;
41
+
23
42
export default class VerdaccioGitLab implements IPluginAuth {
24
43
options : PluginOptions ;
25
44
config : VerdaccioGitlabConfig ;
26
45
authCache : AuthCache ;
27
46
logger : Logger ;
47
+ accessLevel : VerdaccioGitlabAccessLevel ;
48
+ publishLevel : VerdaccioGitlabAccessLevel ;
28
49
29
50
constructor (
30
51
config : VerdaccioGitlabConfig ,
@@ -46,6 +67,17 @@ export default class VerdaccioGitLab implements IPluginAuth {
46
67
this . logger . info ( `[gitlab] initialized auth cache with ttl: ${ ttl } seconds` ) ;
47
68
}
48
69
70
+ this . accessLevel = '$reporter' ;
71
+ if ( this . config . access ) {
72
+ this . accessLevel = this . config . access ;
73
+ }
74
+ this . logger . info ( `[gitlab] access control level: ${ this . config . access || '' } ` ) ;
75
+
76
+ this . publishLevel = '$owner' ;
77
+ if ( this . config . publish ) {
78
+ this . publishLevel = this . config . publish ;
79
+ }
80
+ this . logger . info ( `[gitlab] publish control level: ${ this . config . publish || '' } ` ) ;
49
81
}
50
82
51
83
authenticate ( user : string , password : string , cb : Callback ) {
@@ -55,8 +87,8 @@ export default class VerdaccioGitLab implements IPluginAuth {
55
87
const cachedUserGroups = this . _getCachedUserGroups ( user , password ) ;
56
88
57
89
if ( cachedUserGroups ) {
58
- this . logger . debug ( `[gitlab] user: ${ user } found in cache, authenticated with groups: ${ cachedUserGroups . toString ( ) } ` ) ;
59
- return cb ( null , cachedUserGroups ) ;
90
+ this . logger . debug ( `[gitlab] user: ${ user } found in cache, authenticated with groups:` , cachedUserGroups ) ;
91
+ return cb ( null , cachedUserGroups . publish ) ;
60
92
}
61
93
62
94
// Not found in cache, query gitlab
@@ -67,28 +99,47 @@ export default class VerdaccioGitLab implements IPluginAuth {
67
99
token : password
68
100
} ) ;
69
101
70
- GitlabAPI . Users . current ( ) . then ( response => {
102
+ const pUsers = GitlabAPI . Users . current ( ) ;
103
+ return pUsers . then ( response => {
71
104
if ( user !== response . username ) {
72
105
return cb ( httperror [ 401 ] ( 'wrong gitlab username' ) ) ;
73
106
}
74
107
75
- // Set the groups of an authenticated user to themselves and all gitlab projects of which they are an owner
76
- let ownedGroups = [ user ] ;
77
- GitlabAPI . Groups . all ( { owned : true } ) . then ( groups => {
78
- for ( let group of groups ) {
79
- if ( group . path === group . full_path ) {
80
- ownedGroups . push ( group . path ) ;
81
- }
82
- }
108
+ const accessLevelId = this . config . access ? ACCESS_LEVEL_MAPPING [ this . config . access ] : null ;
109
+ const publishLevelId = this . config . publish ? ACCESS_LEVEL_MAPPING [ this . config . publish ] : null ;
110
+
111
+ const userGroups = {
112
+ access : [ user ] ,
113
+ publish : [ user ]
114
+ } ;
115
+
116
+ // Set the groups of an authenticated user:
117
+ // - for access, themselves and all groups with access level $auth.gitlab.access configuration
118
+ // - for publish, themselves and all groups with access level $auth.gitlab.publish configuration
119
+
120
+ const pAccessGroups = GitlabAPI . Groups . all ( { min_access_level : accessLevelId } ) . then ( groups => {
121
+ this . _addGroupsToArray ( groups , userGroups . access ) ;
122
+ } ) . catch ( error => {
123
+ this . logger . error ( `[gitlab] user: ${ user } error querying access groups: ${ error } ` ) ;
124
+ } ) ;
125
+
126
+ const pPublishGroups = GitlabAPI . Groups . all ( { min_access_level : publishLevelId } ) . then ( groups => {
127
+ this . _addGroupsToArray ( groups , userGroups . publish ) ;
128
+ } ) . catch ( error => {
129
+ this . logger . error ( `[gitlab] user: ${ user } error querying publish groups: ${ error } ` ) ;
130
+ } ) ;
83
131
84
- // Store found groups in cache
85
- this . _setCachedUserGroups ( user , password , ownedGroups ) ;
86
- this . logger . trace ( `[gitlab] saving data in cache for user: ${ user } ` ) ;
132
+ const pGroups = Promise . all ( [ pAccessGroups , pPublishGroups ] ) ;
133
+ return pGroups . then ( ( ) => {
134
+ this . _setCachedUserGroups ( user , password , userGroups ) ;
87
135
88
136
this . logger . info ( `[gitlab] user: ${ user } authenticated` ) ;
89
- this . logger . debug ( `[gitlab] user: ${ user } authenticated, with groups: ${ ownedGroups . toString ( ) } ` ) ;
90
- return cb ( null , ownedGroups ) ;
137
+ this . logger . debug ( `[gitlab] user: ${ user } authenticated, with groups:` , userGroups ) ;
138
+ return cb ( null , userGroups . publish ) ;
139
+ } ) . catch ( error => {
140
+ this . logger . error ( `[gitlab] error authenticating: ${ error } ` ) ;
91
141
} ) ;
142
+
92
143
} ) . catch ( error => {
93
144
this . logger . info ( `[gitlab] user: ${ user } error authenticating: ${ error . message || { } } ` ) ;
94
145
if ( error ) {
@@ -104,68 +155,83 @@ export default class VerdaccioGitLab implements IPluginAuth {
104
155
105
156
allow_access ( user : RemoteUser , _package : VerdaccioGitlabPackageAccess , cb : Callback ) {
106
157
if ( ! _package . gitlab ) { return cb ( ) ; }
158
+
107
159
if ( ( _package . access || [ ] ) . includes ( '$authenticated' ) && user . name !== undefined ) {
108
160
this . logger . debug ( `[gitlab] allow user: ${ user . name } access to package: ${ _package . name } ` ) ;
109
161
return cb ( null , true ) ;
110
- } else if ( ! ( _package . access || [ ] ) . includes ( '$authenticated' ) ) {
162
+ } else if ( ! ( _package . access || [ ] ) . includes ( '$authenticated' ) ) {
111
163
this . logger . debug ( `[gitlab] allow unauthenticated access to package: ${ _package . name } ` ) ;
112
164
return cb ( null , true ) ;
113
165
} else {
114
166
this . logger . debug ( `[gitlab] deny user: ${ user . name || '' } access to package: ${ _package . name } ` ) ;
115
167
return cb ( null , false ) ;
116
168
}
169
+
117
170
}
118
171
119
172
allow_publish ( user : RemoteUser , _package : VerdaccioGitlabPackageAccess , cb : Callback ) {
120
173
if ( ! _package . gitlab ) { return cb ( ) ; }
121
- let packageScopeOwner = false ;
122
- let packageOwner = false ;
174
+ let packageScopePermit = false ;
175
+ let packagePermit = false ;
123
176
124
177
// Only allow to publish packages when:
125
178
// - the package has exactly the same name as one of the user groups, or
126
179
// - the package scope is the same as one of the user groups
127
180
for ( let real_group of user . real_groups ) { // jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
128
181
this . logger . trace ( `[gitlab] publish: checking group: ${ real_group } for user: ${ user . name || '' } and package: ${ _package . name } ` ) ;
129
182
if ( real_group === _package . name ) {
130
- packageOwner = true ;
183
+ packagePermit = true ;
131
184
break ;
132
185
} else {
133
186
if ( _package . name . indexOf ( '@' ) === 0 ) {
134
187
if ( real_group === _package . name . slice ( 1 , _package . name . lastIndexOf ( '/' ) ) ) {
135
- packageScopeOwner = true ;
188
+ packageScopePermit = true ;
136
189
break ;
137
190
}
138
191
}
139
192
}
140
193
}
141
194
142
- if ( packageOwner === true ) {
143
- this . logger . debug ( `[gitlab] user: ${ user . name || '' } allowed to publish package: ${ _package . name } as owner of package-name` ) ;
195
+ if ( packagePermit === true ) {
196
+ this . logger . debug ( `[gitlab] user: ${ user . name || '' } allowed to publish package: ${ _package . name } based on package-name` ) ;
144
197
return cb ( null , false ) ;
145
198
} else {
146
- if ( packageScopeOwner === true ) {
147
- this . logger . debug ( `[gitlab] user: ${ user . name || '' } allowed to publish package: ${ _package . name } as owner of package-scope` ) ;
199
+ if ( packageScopePermit === true ) {
200
+ this . logger . debug ( `[gitlab] user: ${ user . name || '' } allowed to publish package: ${ _package . name } based on package-scope` ) ;
148
201
return cb ( null , false ) ;
149
202
} else {
150
203
this . logger . debug ( `[gitlab] user: ${ user . name || '' } denied from publishing package: ${ _package . name } ` ) ;
151
204
if ( _package . name . indexOf ( '@' ) === 0 ) {
152
- return cb ( httperror [ 403 ] ( ' must be owner of package-scope' ) ) ;
205
+ return cb ( httperror [ 403 ] ( ` must have required permissions: ${ this . config . publish || '' } at package-scope` ) ) ;
153
206
} else {
154
- return cb ( httperror [ 403 ] ( ' must be owner of package-name' ) ) ;
207
+ return cb ( httperror [ 403 ] ( ` must have required permissions: ${ this . config . publish || '' } at package-name` ) ) ;
155
208
}
156
209
}
157
210
}
158
211
}
159
212
160
- _getCachedUserGroups ( username : string , password : string ) : ?string [ ] {
213
+ _getCachedUserGroups ( username : string , password : string ) : ?UserDataGroups {
161
214
if ( ! this . authCache ) {
162
215
return null ;
163
216
}
164
217
const userData = this . authCache . findUser ( username , password ) ;
165
218
return ( userData || { } ) . groups || null ;
166
219
}
167
220
168
- _setCachedUserGroups ( username : string , password : string , groups : string [ ] ) : boolean {
169
- return this . authCache && this . authCache . storeUser ( username , password , new UserData ( username , groups ) ) ;
221
+ _setCachedUserGroups ( username : string , password : string , groups : UserDataGroups ) : boolean {
222
+ if ( ! this . authCache ) {
223
+ return false ;
224
+ }
225
+ this . logger . trace ( `[gitlab] saving data in cache for user: ${ username } ` ) ;
226
+ return this . authCache . storeUser ( username , password , new UserData ( username , groups ) ) ;
227
+ }
228
+
229
+ _addGroupsToArray ( src : { path : string , full_path : string } [ ] , dst : string [ ] ) {
230
+ src . forEach ( group => {
231
+ if ( group . path === group . full_path ) {
232
+ dst . push ( group . path ) ;
233
+ }
234
+ } ) ;
170
235
}
236
+
171
237
}
0 commit comments