1 /*
2 ** Copyright (c) 2018 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 **
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 ** drh@hwaci.com
14 ** http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains code used managing user capability strings.
19 */
20 #include "config.h"
21 #include "capabilities.h"
22 #include <assert.h>
23
24 #if INTERFACE
25 /*
26 ** A capability string object holds all defined capabilities in a
27 ** vector format that is subject to boolean operations.
28 */
29 struct CapabilityString {
30 unsigned char x[128];
31 };
32 #endif
33
34 /*
35 ** Add capabilities to a CapabilityString. If pIn is NULL, then create
36 ** a new capability string.
37 **
38 ** Call capability_free() on the allocated CapabilityString object to
39 ** deallocate.
40 */
capability_add(CapabilityString * pIn,const char * zCap)41 CapabilityString *capability_add(CapabilityString *pIn, const char *zCap){
42 int c;
43 int i;
44 if( pIn==0 ){
45 pIn = fossil_malloc( sizeof(*pIn) );
46 memset(pIn, 0, sizeof(*pIn));
47 }
48 if( zCap ){
49 for(i=0; (c = zCap[i])!=0; i++){
50 if( c>='0' && c<='z' ) pIn->x[c] = 1;
51 }
52 }
53 return pIn;
54 }
55
56 /*
57 ** Remove capabilities from a CapabilityString.
58 */
capability_remove(CapabilityString * pIn,const char * zCap)59 CapabilityString *capability_remove(CapabilityString *pIn, const char *zCap){
60 int c;
61 int i;
62 if( pIn==0 ){
63 pIn = fossil_malloc( sizeof(*pIn) );
64 memset(pIn, 0, sizeof(*pIn));
65 }
66 if( zCap ){
67 for(i=0; (c = zCap[i])!=0; i++){
68 if( c>='0' && c<='z' ) pIn->x[c] = 0;
69 }
70 }
71 return pIn;
72 }
73
74 /*
75 ** Return true if any of the capabilities in zNeeded are found in pCap
76 */
capability_has_any(CapabilityString * p,const char * zNeeded)77 int capability_has_any(CapabilityString *p, const char *zNeeded){
78 if( p==0 ) return 0;
79 if( zNeeded==0 ) return 0;
80 while( zNeeded[0] ){
81 int c = zNeeded[0];
82 if( fossil_isalnum(c) && p->x[c] ) return 1;
83 zNeeded++;
84 }
85 return 0;
86 }
87
88 /*
89 ** Delete a CapabilityString object.
90 */
capability_free(CapabilityString * p)91 void capability_free(CapabilityString *p){
92 fossil_free(p);
93 }
94
95 /*
96 ** Expand the capability string by including all capabilities for
97 ** special users "nobody" and "anonymous". Also include "reader"
98 ** if "u" is present and "developer" if "v" is present.
99 */
capability_expand(CapabilityString * pIn)100 void capability_expand(CapabilityString *pIn){
101 static char *zNobody = 0;
102 static char *zAnon = 0;
103 static char *zReader = 0;
104 static char *zDev = 0;
105 static char *zAdmin = "bcdefghijklmnopqrtwz234567AD";
106 int doneV = 0;
107
108 if( pIn==0 ){
109 fossil_free(zNobody); zNobody = 0;
110 fossil_free(zAnon); zAnon = 0;
111 fossil_free(zReader); zReader = 0;
112 fossil_free(zDev); zDev = 0;
113 return;
114 }
115 if( zNobody==0 ){
116 zNobody = db_text(0, "SELECT cap FROM user WHERE login='nobody'");
117 zAnon = db_text(0, "SELECT cap FROM user WHERE login='anonymous'");
118 zReader = db_text(0, "SELECT cap FROM user WHERE login='reader'");
119 zDev = db_text(0, "SELECT cap FROM user WHERE login='developer'");
120 }
121 pIn = capability_add(pIn, zAnon);
122 pIn = capability_add(pIn, zNobody);
123 if( pIn->x['a'] || pIn->x['s'] ){
124 pIn = capability_add(pIn, zAdmin);
125 }
126 if( pIn->x['v'] ){
127 pIn = capability_add(pIn, zDev);
128 doneV = 1;
129 }
130 if( pIn->x['u'] ){
131 pIn = capability_add(pIn, zReader);
132 if( pIn->x['v'] && !doneV ){
133 pIn = capability_add(pIn, zDev);
134 }
135 }
136 }
137
138 /*
139 ** Render a capability string in canonical string format. Space to hold
140 ** the returned string is obtained from fossil_malloc() can should be freed
141 ** by the caller.
142 */
capability_string(CapabilityString * p)143 char *capability_string(CapabilityString *p){
144 Blob out;
145 int i;
146 int j = 0;
147 char buf[100];
148 blob_init(&out, 0, 0);
149 for(i='a'; i<='z'; i++){
150 if( p->x[i] ) buf[j++] = i;
151 }
152 for(i='0'; i<='9'; i++){
153 if( p->x[i] ) buf[j++] = i;
154 }
155 for(i='A'; i<='Z'; i++){
156 if( p->x[i] ) buf[j++] = i;
157 }
158 buf[j] = 0;
159 return fossil_strdup(buf);
160 }
161
162 /*
163 ** The next two routines implement an aggregate SQL function that
164 ** takes multiple capability strings and in the end returns their
165 ** union. Example usage:
166 **
167 ** SELECT capunion(cap) FROM user WHERE login IN ('nobody','anonymous');
168 */
capability_union_step(sqlite3_context * context,int argc,sqlite3_value ** argv)169 void capability_union_step(
170 sqlite3_context *context,
171 int argc,
172 sqlite3_value **argv
173 ){
174 CapabilityString *p;
175 const char *zIn;
176
177 zIn = (const char*)sqlite3_value_text(argv[0]);
178 if( zIn==0 ) return;
179 p = (CapabilityString*)sqlite3_aggregate_context(context, sizeof(*p));
180 p = capability_add(p, zIn);
181 }
capability_union_finalize(sqlite3_context * context)182 void capability_union_finalize(sqlite3_context *context){
183 CapabilityString *p;
184 p = sqlite3_aggregate_context(context, 0);
185 if( p ){
186 char *zOut = capability_string(p);
187 sqlite3_result_text(context, zOut, -1, fossil_free);
188 }
189 }
190
191 /*
192 ** The next routines takes the raw USER.CAP field and expands it with
193 ** capabilities from special users. Example:
194 **
195 ** SELECT fullcap(cap) FROM user WHERE login=?1
196 */
capability_fullcap(sqlite3_context * context,int argc,sqlite3_value ** argv)197 void capability_fullcap(
198 sqlite3_context *context,
199 int argc,
200 sqlite3_value **argv
201 ){
202 CapabilityString *p;
203 const char *zIn;
204 char *zOut;
205
206 zIn = (const char*)sqlite3_value_text(argv[0]);
207 if( zIn==0 ) zIn = "";
208 p = capability_add(0, zIn);
209 capability_expand(p);
210 zOut = capability_string(p);
211 sqlite3_result_text(context, zOut, -1, fossil_free);
212 capability_free(p);
213 }
214
215 #if INTERFACE
216 /*
217 ** Capabilities are grouped into "classes" as follows:
218 */
219 #define CAPCLASS_CODE 0x0001
220 #define CAPCLASS_WIKI 0x0002
221 #define CAPCLASS_TKT 0x0004
222 #define CAPCLASS_FORUM 0x0008
223 #define CAPCLASS_DATA 0x0010
224 #define CAPCLASS_ALERT 0x0020
225 #define CAPCLASS_OTHER 0x0040
226 #define CAPCLASS_SUPER 0x0080
227 #define CAPCLASS_ALL 0xffff
228 #endif /* INTERFACE */
229
230
231 /*
232 ** The following structure holds descriptions of the various capabilities.
233 */
234 static struct Caps {
235 char cCap; /* The capability letter */
236 unsigned short eClass; /* The "class" for this capability */
237 unsigned nUser; /* Number of users with this capability */
238 char *zAbbrev; /* Abbreviated mnemonic name */
239 char *zOneLiner; /* One-line summary */
240 } aCap[] = {
241 { 'a', CAPCLASS_SUPER, 0,
242 "Admin", "Create and delete users" },
243 { 'b', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
244 "Attach", "Add attachments to wiki or tickets" },
245 { 'c', CAPCLASS_TKT, 0,
246 "Append-Tkt", "Append to existing tickets" },
247 /*
248 ** d unused since fork from CVSTrac;
249 ** see https://fossil-scm.org/forum/forumpost/43c78f4bef
250 */
251 { 'e', CAPCLASS_DATA, 0,
252 "View-PII", "View sensitive info such as email addresses" },
253 { 'f', CAPCLASS_WIKI, 0,
254 "New-Wiki", "Create new wiki pages" },
255 { 'g', CAPCLASS_DATA, 0,
256 "Clone", "Clone the repository" },
257 { 'h', CAPCLASS_OTHER, 0,
258 "Hyperlinks", "Show hyperlinks to detailed repository history" },
259 { 'i', CAPCLASS_CODE, 0,
260 "Check-In", "Check-in code changes" },
261 { 'j', CAPCLASS_WIKI, 0,
262 "Read-Wiki", "View wiki pages" },
263 { 'k', CAPCLASS_WIKI, 0,
264 "Write-Wiki", "Edit wiki pages" },
265 { 'l', CAPCLASS_WIKI|CAPCLASS_SUPER, 0,
266 "Mod-Wiki", "Moderator for wiki pages" },
267 { 'm', CAPCLASS_WIKI, 0,
268 "Append-Wiki", "Append to wiki pages" },
269 { 'n', CAPCLASS_TKT, 0,
270 "New-Tkt", "Create new tickets" },
271 { 'o', CAPCLASS_CODE, 0,
272 "Check-Out", "Check out code" },
273 { 'p', CAPCLASS_OTHER, 0,
274 "Password", "Change your own password" },
275 { 'q', CAPCLASS_TKT|CAPCLASS_SUPER, 0,
276 "Mod-Tkt", "Moderate tickets" },
277 { 'r', CAPCLASS_TKT, 0,
278 "Read-Tkt", "View tickets" },
279 { 's', CAPCLASS_SUPER, 0,
280 "Superuser", "Setup and configure the respository" },
281 { 't', CAPCLASS_TKT, 0,
282 "Reports", "Create new ticket report formats" },
283 { 'u', CAPCLASS_OTHER, 0,
284 "Reader", "Inherit all the capabilities of the \"reader\" user" },
285 { 'v', CAPCLASS_OTHER, 0,
286 "Developer", "Inherit all capabilities of the \"developer\" user" },
287 { 'w', CAPCLASS_TKT, 0,
288 "Write-Tkt", "Edit tickets" },
289 { 'x', CAPCLASS_DATA, 0,
290 "Private", "Push and/or pull private branches" },
291 { 'y', CAPCLASS_SUPER, 0,
292 "Write-UV", "Push unversioned content" },
293 { 'z', CAPCLASS_CODE, 0,
294 "Zip-Download", "Download a ZIP archive, tarball, or SQL archive" },
295 { '2', CAPCLASS_FORUM, 0,
296 "Forum-Read", "Read forum posts by others" },
297 { '3', CAPCLASS_FORUM, 0,
298 "Forum-Write", "Create new forum messages" },
299 { '4', CAPCLASS_FORUM, 0,
300 "Forum-Trusted", "Create forum messages that bypass moderation" },
301 { '5', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
302 "Forum-Mod", "Moderator for forum messages" },
303 { '6', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
304 "Forum-Admin", "Grant capability '4' to other users" },
305 { '7', CAPCLASS_ALERT, 0,
306 "Alerts", "Sign up for email alerts" },
307 { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
308 "Announce", "Send announcements to all subscribers" },
309 { 'C', CAPCLASS_FORUM, 0,
310 "Chat", "Read and/or writes messages in the chatroom" },
311 { 'D', CAPCLASS_OTHER, 0,
312 "Debug", "Enable debugging features" },
313 };
314
315 /*
316 ** Populate the aCap[].nUser values based on the current content
317 ** of the USER table.
318 */
capabilities_count(void)319 void capabilities_count(void){
320 int i;
321 static int done = 0;
322 Stmt q;
323 if( done ) return;
324 db_prepare(&q, "SELECT fullcap(cap) FROM user");
325 while( db_step(&q)==SQLITE_ROW ){
326 const char *zCap = db_column_text(&q, 0);
327 if( zCap==0 || zCap[0]==0 ) continue;
328 for(i=0; i<sizeof(aCap)/sizeof(aCap[0]); i++){
329 if( strchr(zCap, aCap[i].cCap) ) aCap[i].nUser++;
330 }
331 }
332 db_finalize(&q);
333 done = 1;
334 }
335
336
337 /*
338 ** Generate HTML that lists all of the capability letters together with
339 ** a brief summary of what each letter means.
340 */
capabilities_table(unsigned mClass)341 void capabilities_table(unsigned mClass){
342 int i;
343 if( g.perm.Admin ) capabilities_count();
344 @ <table>
345 @ <tbody>
346 for(i=0; i<sizeof(aCap)/sizeof(aCap[0]); i++){
347 int n;
348 if( (aCap[i].eClass & mClass)==0 ) continue;
349 @ <tr><th valign="top">%c(aCap[i].cCap)</th>
350 @ <td>%h(aCap[i].zAbbrev)</td><td>%h(aCap[i].zOneLiner)</td>\
351 n = aCap[i].nUser;
352 if( n && g.perm.Admin ){
353 @ <td><a href="%R/setup_ulist?with=%c(aCap[i].cCap)">\
354 @ %d(n) user%s(n>1?"s":"")</a></td>\
355 }
356 @ </tr>
357 }
358 @ </tbody>
359 @ </table>
360 }
361
362 /*
363 ** Generate a "capability summary table" that shows the major capabilities
364 ** against the various user categories.
365 */
capability_summary(void)366 void capability_summary(void){
367 Stmt q;
368 CapabilityString *pCap;
369 char *zSelfCap;
370 char *zPubPages = db_get("public-pages",0);
371 int hasPubPages = zPubPages && zPubPages[0];
372
373 pCap = capability_add(0, db_get("default-perms","u"));
374 capability_expand(pCap);
375 zSelfCap = capability_string(pCap);
376 capability_free(pCap);
377
378 db_prepare(&q,
379 "WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3),"
380 "('developer',4))"
381 " SELECT id, CASE WHEN user.login='nobody' THEN user.cap"
382 " ELSE fullcap(user.cap) END,seq,1"
383 " FROM t LEFT JOIN user ON t.id=user.login"
384 " UNION ALL"
385 " SELECT 'Public Pages', %Q, 100, %d"
386 " UNION ALL"
387 " SELECT 'New User Default', %Q, 110, 1"
388 " UNION ALL"
389 " SELECT 'Regular User', fullcap(capunion(cap)), 200, count(*) FROM user"
390 " WHERE cap NOT GLOB '*[as]*' AND login NOT IN (SELECT id FROM t)"
391 " UNION ALL"
392 " SELECT 'Adminstrator', fullcap(capunion(cap)), 300, count(*) FROM user"
393 " WHERE cap GLOB '*[as]*'"
394 " ORDER BY 3 ASC",
395 zSelfCap, hasPubPages, zSelfCap
396 );
397 @ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1">
398 @ <tr><th> <th>Code<th>Forum<th>Tickets<th>Wiki<th>Chat\
399 @ <th>Unversioned Content</th></tr>
400 while( db_step(&q)==SQLITE_ROW ){
401 const char *zId = db_column_text(&q, 0);
402 const char *zCap = db_column_text(&q, 1);
403 int n = db_column_int(&q, 3);
404 int eType;
405 static const char *const azType[] = { "off", "read", "write" };
406 static const char *const azClass[] =
407 { "capsumOff", "capsumRead", "capsumWrite" };
408
409 if( n==0 ) continue;
410
411 /* Code */
412 if( db_column_int(&q,2)<10 ){
413 @ <tr><th align="right"><tt>"%h(zId)"</tt></th>
414 }else if( n>1 ){
415 @ <tr><th align="right">%d(n) %h(zId)s</th>
416 }else{
417 @ <tr><th align="right">%h(zId)</th>
418 }
419 if( sqlite3_strglob("*[asi]*",zCap)==0 ){
420 eType = 2;
421 }else if( sqlite3_strglob("*[oz]*",zCap)==0 ){
422 eType = 1;
423 }else{
424 eType = 0;
425 }
426 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
427
428 /* Forum */
429 if( sqlite3_strglob("*[as3456]*",zCap)==0 ){
430 eType = 2;
431 }else if( sqlite3_strglob("*2*",zCap)==0 ){
432 eType = 1;
433 }else{
434 eType = 0;
435 }
436 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
437
438 /* Ticket */
439 if( sqlite3_strglob("*[ascnqtw]*",zCap)==0 ){
440 eType = 2;
441 }else if( sqlite3_strglob("*r*",zCap)==0 ){
442 eType = 1;
443 }else{
444 eType = 0;
445 }
446 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
447
448 /* Wiki */
449 if( sqlite3_strglob("*[asdfklm]*",zCap)==0 ){
450 eType = 2;
451 }else if( sqlite3_strglob("*j*",zCap)==0 ){
452 eType = 1;
453 }else{
454 eType = 0;
455 }
456 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
457
458 /* Chat */
459 if( sqlite3_strglob("*C*",zCap)==0 ){
460 eType = 2;
461 }else{
462 eType = 0;
463 }
464 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
465
466 /* Unversioned */
467 if( sqlite3_strglob("*y*",zCap)==0 ){
468 eType = 2;
469 }else if( sqlite3_strglob("*[ioas]*",zCap)==0 ){
470 eType = 1;
471 }else{
472 eType = 0;
473 }
474 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
475
476 }
477 db_finalize(&q);
478 @ </table>
479 }
480