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>&nbsp;<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