diff --git a/internal/assets/templates/templates_gen.go b/internal/assets/templates/templates_gen.go index b5719801c..6d786759a 100644 --- a/internal/assets/templates/templates_gen.go +++ b/internal/assets/templates/templates_gen.go @@ -1,6 +1,6 @@ // Code generated by go-bindata. DO NOT EDIT. // sources: -// ../../../templates/admin/auth/edit.tmpl (10.544kB) +// ../../../templates/admin/auth/edit.tmpl (10.533kB) // ../../../templates/admin/auth/list.tmpl (2.154kB) // ../../../templates/admin/auth/new.tmpl (10.045kB) // ../../../templates/admin/base/page.tmpl (1.227kB) @@ -217,7 +217,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _adminAuthEditTmpl = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x5a\x6d\x6f\xe3\xb8\x11\xfe\xec\xfd\x15\xac\x7a\x2d\x76\x81\x8b\x8d\xa2\x87\x43\x71\xb0\x17\xc8\x5d\x76\x6f\x17\x48\x0e\x41\x93\xed\x57\x81\x16\xc7\x16\x1b\x89\x54\x49\x2a\xd9\xc0\xf5\x7f\x2f\xf8\x26\x89\x7a\xa1\xe5\x4d\x0e\xbd\x2f\x89\x44\xce\x90\xf3\x3c\x24\x47\x33\x63\x1e\x0e\x0a\xca\xaa\xc0\x0a\x50\xb2\xc5\x12\x56\x39\x60\x92\xa0\xe5\xf1\xf8\x66\x4d\xe8\x23\xca\x0a\x2c\xe5\x26\xc1\xa4\xa4\x0c\x01\xa1\x0a\xe1\x5a\xe5\xc0\x14\xcd\xb0\xa2\x9c\x25\xef\xdf\x2c\xba\x82\x35\x45\x19\x67\x0a\x53\x06\x42\xf7\xf5\x3b\xf7\x82\x12\xd3\xbe\xe8\xce\x6c\x86\x5f\x31\xfc\xb8\xc5\xc2\x4e\xbe\x08\x35\xd5\x13\x14\x8f\x80\x9e\x28\x01\x94\xf1\xa2\x2e\x99\x99\x06\x98\xb2\x83\x2d\x06\x38\x70\x01\x42\x35\x63\x2d\xd6\xf9\x0f\x1d\x2b\x14\xaf\x10\x56\x0a\x67\x39\x10\xa4\x11\x3b\x63\xcd\x40\x4b\xfa\xb7\x7f\xb0\xe5\xbd\x70\x66\x2d\x35\x60\xb9\xd4\xd8\x13\x3f\xd8\x2a\xff\xc1\x8a\xf7\xd0\x35\x63\x4a\xd8\x97\xad\x71\x8b\xf5\x8e\x8b\xb2\x23\xa7\x5f\x13\x84\x33\xcd\xe0\x26\x39\x1c\x96\xd7\x94\x3d\x1c\x8f\x09\x2a\x41\xe5\x9c\x6c\x92\x8a\xcb\x46\x59\x9b\xf4\xcb\xdd\x3f\x3f\xde\xf3\x07\x60\x9f\xee\x6f\xae\x9d\x15\x8b\xc5\x9a\xb2\xaa\x56\x48\x3d\x57\xb0\x49\x72\x4a\x08\xb0\x04\x31\x5c\xc2\x26\xa1\x24\x41\x8f\xb8\xa8\xc1\x0c\x7f\xc7\x6b\x91\xc1\xf2\xf3\xd5\xf1\xd8\x8c\xda\x35\x9d\xb2\x82\x32\x40\x3b\x0a\x05\x69\x04\x16\xeb\x02\x6f\xa1\x78\x7f\x38\x7c\x37\x4e\x89\xfe\x9b\xea\xc9\x93\xe3\x71\xbd\xb2\xc2\x8d\xee\x98\x69\x94\x6c\x92\x56\xc9\x59\x6a\x9f\x07\xb6\xde\x3f\x57\xd0\xb1\x76\xb1\x96\x15\x66\xef\xc3\xfe\xdf\x70\x09\x7a\x6a\xd3\xe5\x61\xad\x08\x7d\x1c\xc3\x28\xe0\x3f\x35\x15\x40\x50\x17\x2c\x3a\x1c\xe8\x0e\x2d\x3f\x08\x91\xda\xc1\x40\x08\x2e\x0e\x07\x60\x24\x98\xdc\x80\xd3\xcb\xb6\x49\xb4\xd5\xc9\xfb\xa9\x6d\x62\xe0\x19\x91\x49\x4e\x34\x0b\x46\xc2\x11\x60\x9f\x07\x04\x58\x7b\x12\x7d\xde\xf8\x8e\x67\xb5\x44\x1e\x41\x0f\xaa\x7f\xfb\xd3\xc5\x05\xba\xbe\xba\xbc\x45\x98\x11\x74\x65\x9e\x2e\x2e\xda\x3d\x44\x77\x88\x0b\xd4\x6c\x05\x69\x04\xda\x57\xa3\xd0\xec\xac\xc5\xe1\x80\xbe\xcb\x76\xfb\x9f\x36\x5e\xc2\x88\xb7\xfd\x23\xbb\xa7\x21\xb8\xcf\xec\x1d\x64\xb5\xa0\xea\xf9\x56\x70\xc5\x33\x5e\x4c\xb1\xdc\x6e\xb8\x71\x6e\xa5\x1b\x27\xad\xdc\x40\x23\x1c\xf7\x4f\xa4\x84\x02\xcc\x31\x43\x5e\xfb\xc2\x6b\x23\x22\x78\x45\xf8\x13\xeb\x58\x30\xbd\x6f\x87\x93\xbb\xe5\x1b\xe9\x68\xd6\x52\x53\xb8\x1c\xc2\x0f\x26\xec\x7a\x39\xf8\xaa\xf4\xd6\x1a\x55\xf3\x9b\xbd\xb3\xbd\x8d\xbd\x5e\xd9\xa3\x41\x34\xd3\x6e\x79\xbd\xa2\x13\xb3\x94\xc0\xea\xae\x05\x8b\xc3\x41\x60\xb6\x07\x34\x98\x52\xb6\xeb\x3d\x58\x73\x05\x65\x82\x08\x56\xf8\xa2\xdd\xb9\xfe\xcc\x1e\x0e\xcb\x71\x6b\x17\x6e\xcd\x3b\x86\x85\x12\xe1\x6b\xef\x6d\xec\x30\xf7\x5c\x56\x70\x52\x73\xe3\x44\xa7\x76\x93\xe9\x1d\xdb\x40\xed\x29\x35\x22\x6e\x99\xed\x73\xb8\xb2\x9f\xb8\x54\xfa\x88\x56\x05\xce\x20\xe7\x05\x01\xb1\x49\x60\xb9\x5f\xa2\xf2\x99\xf0\x12\x53\xb6\xcc\x78\x99\x0c\x0e\xee\x4b\x81\x55\x5c\x44\x80\x99\xde\x38\x30\x23\xe2\x80\xd9\xe7\x10\xd8\x2d\x17\x06\xd8\x08\xb2\x1f\xff\xfe\xe3\x49\x40\xf6\xe8\x07\x9e\xa6\xb3\xe6\x5d\xb8\x7d\x94\x01\xcc\x2d\x65\x24\x25\x6c\x1a\xa9\x17\x18\x01\xdb\x45\xeb\xc5\x1c\xe0\xe6\x35\xc4\xfc\x33\x65\xe4\xea\xb7\xf1\xe5\xcc\xd8\xe6\x0e\xb0\xc8\xf2\xef\x49\xb6\xf1\x6b\xab\x9f\xf5\xf2\x76\x27\xad\x3c\xb0\x1c\x8a\x0a\xe9\xf3\x8c\x04\x90\x93\x08\x52\x2d\x0e\xc2\x00\xa9\xa6\x4e\x83\x87\xe4\xa9\xc3\x0f\x90\x38\x47\x55\x61\x29\x9f\xb8\x08\xf6\xcb\x59\x2c\xb7\x03\x44\x2d\x6d\xc4\xe6\x30\xde\x08\x77\x79\x6f\x1b\x7b\x96\x8f\xac\xc6\xad\xeb\xeb\x39\xcc\x6f\xe2\xd8\xcf\x33\x93\xe9\x39\x27\x32\x60\xb1\x96\x20\x52\x1d\x78\x4e\xdb\xd1\x8a\x9c\x60\xaf\x15\x74\xcc\x75\x1a\x42\x96\xbe\x48\x10\x3f\x63\x09\xe3\xbb\x96\xd7\x1b\x2d\x20\xc7\x36\xed\xf0\x08\x0f\xce\x70\xe0\xa9\x7b\x47\xfa\x2a\x72\xa6\xcf\x20\x2c\x76\xb8\xbd\xc0\x1c\xb2\xda\xc3\xdd\xbc\x0e\x89\x9a\x3a\xdc\x35\x25\x9b\xbf\xc8\xef\x5f\x93\xad\xb3\x3d\xfa\x8e\x16\x4a\xe7\x20\x53\x64\xb8\xfe\xb8\x57\x77\x42\x8e\x09\xff\x16\x12\xf1\xd1\xb4\x8e\x13\xf1\xf6\xaf\x6f\xf9\xf6\xdf\x90\xa9\x5f\x8c\xe5\x15\x97\xf4\xeb\x65\x96\xf1\x9a\xa9\x77\x6f\x2d\x4b\xef\xde\x9d\xf5\x35\x8b\x41\x36\xf0\xd2\x53\xc0\x03\xa9\x38\xfc\x40\xd4\x91\x10\xb6\x85\x54\x5c\xea\xbe\x86\x8f\x97\xa2\x51\x4a\xd0\x6d\xad\x20\xd5\x3b\xf0\x44\x8e\x30\x94\x3d\x81\x6c\xa8\xe0\xf1\x8d\xf4\xf4\x50\x7a\x89\x2f\x4e\x60\xb0\xf6\xf3\xcd\x4c\x3b\x7a\xc9\x6b\x72\x36\x97\xaf\xb3\xb8\x1a\xe7\x29\xca\x91\xcb\xb9\x5e\x0d\x97\xac\x67\x6f\x05\x2f\x3a\x17\x9d\x97\x1f\x00\x6c\x3a\x26\x30\xde\xd5\x7e\x1b\xbc\x56\x38\xda\xce\x5d\x62\x5a\xcc\x01\x6b\xe4\xe6\x22\x35\xc2\x03\x98\xb6\x75\x02\xe3\x0d\xa6\xc5\x44\x60\x6e\xd4\xa6\x5c\x58\x24\xbb\x1d\x10\xd0\x2b\x7a\xe5\x90\x3d\x6c\xf9\xd7\xe1\xe7\xee\xfd\x5a\x2a\xc1\xd9\x7e\x92\x96\x47\x10\x74\xf7\x9c\xee\x05\xaf\xab\xb4\x84\x72\x0b\x42\xe6\xb4\x32\xfc\x38\xd5\xc9\x2f\xa0\x33\x00\x18\xde\x16\x70\x21\x9f\xa5\x49\xcb\x6c\x68\xd5\x98\xe4\xb8\xb3\x13\x58\x51\xe2\x72\x37\x85\xc5\x1e\xd4\x26\xf9\xb3\xed\x34\xd2\xe6\x53\x6f\xf8\xfc\x55\x37\x7e\xb0\x0a\xc7\xa3\x19\x0f\x88\xfb\xd4\x9d\x9f\xb3\x99\x6c\xdc\x94\xc5\xba\x15\x02\xc6\xd5\xd8\x64\x84\x4a\xf3\xe4\x8b\x04\x66\x4f\xb4\x46\x7e\x43\x9c\x6b\x95\x63\x11\x87\x95\x90\x26\xd0\x37\xe1\xd6\x8c\xf0\xa3\x19\x35\x20\x79\x18\x80\x18\x74\x53\x11\x08\xaf\x37\x46\x2f\x9e\x5d\x44\x62\xd4\x19\xc0\x4f\x7d\x68\x03\xa9\x59\xa0\xc3\x2f\x6d\xd8\x36\x02\x3e\x1a\x79\xfc\xf7\x6d\xc6\x36\x7b\xbe\x97\xe6\x5b\x23\xdf\xe9\x57\x63\x9d\x8e\x38\x5e\x8b\x02\x7b\xb6\xd2\x9a\x46\x92\x05\x2b\xd9\xba\x19\x57\xcc\x36\x66\xa5\x05\x1d\x2f\x19\x0c\xb9\xe9\x4c\x15\xf0\xd3\x6d\x1f\xe1\xe8\xc6\x74\x7f\x31\x75\xda\x31\xe7\x65\xbb\x29\x79\x05\x4a\x0c\xa0\x28\x15\x46\xa2\x65\x42\x83\x07\x92\x52\x66\x3d\xd5\xac\xb8\xbc\x83\xbf\x7d\x1f\x46\xe6\x93\x80\xeb\x08\xd4\x6f\x2d\x39\x4c\xf8\xf3\x39\x0e\x7d\xae\x47\x6f\x48\x93\x9a\x2e\x9d\x7f\xc6\x9d\xb9\xe7\xad\xf7\x8d\x6b\xb5\x07\x1e\xbd\xf5\xd1\xcd\x37\x4f\x7e\x66\x3a\x69\x9e\xf6\xd3\xf1\x5a\x5b\x98\xc1\xf8\xb7\x6e\x6d\xf9\xee\xe6\x7e\x50\x51\x6e\x19\xd7\xbd\x91\xf2\xb1\x51\x3e\xa3\x7c\x7c\x46\x55\xb8\x54\x55\xaa\x1f\xcf\xaa\x06\x6b\x42\xcf\x2c\x00\x37\xf3\xf8\xc2\x6f\xdb\xd0\x0b\x43\x6a\x95\xeb\xfd\x3c\x4c\x1a\xa7\xab\xbd\x56\xe7\xf7\xad\xee\xde\xdc\xdf\xea\x69\xce\xad\xea\xba\x8a\xee\xff\xbb\x9a\x6b\xe8\x8e\x97\x74\xb5\xc8\x8c\xb2\x6e\x3b\x52\x77\x25\x63\x05\xde\xd7\x2e\xe0\x9a\x09\xe3\x55\x5c\x2d\x32\xa3\x92\xdb\x8e\xd4\xc5\x12\xab\xe9\xbe\x56\xfa\x5e\x14\xfc\x09\x48\x6a\xa3\x15\x19\x89\xfa\x7b\x82\x27\xc2\xfe\x9e\xb4\xf7\x89\xfd\xe6\xde\x89\xb3\xdd\x57\xb6\x37\xfc\x4d\x29\x28\x17\xce\x36\x73\xbc\x4e\x18\x50\x35\xe6\x23\x6f\x2f\x6f\xa6\x5d\xe4\xed\xe5\x4d\xc4\x43\x6a\xd5\x17\x54\x90\x2a\x5c\xa6\x12\xc4\x23\xcd\x4e\xa5\xd3\x03\xc9\x13\xbf\x15\xf4\xc5\xfd\xef\x06\x83\xf6\xfe\xcf\x5e\xa6\xcf\xff\x8c\x19\xdf\x76\x63\x5c\xfe\x4a\xd5\xa7\x7a\x3b\x4d\xa7\xed\x8f\x30\xea\x06\x78\x01\xa9\x7b\xaa\xf2\x7a\x9b\xe2\x8a\xa6\xc0\x48\xc5\x29\x8b\x9c\xd8\x31\xe1\x38\xb5\x63\x1a\x3e\x5c\x1c\xeb\xea\x6d\xfa\xdb\xcf\x1f\x5c\xd7\x78\xf4\x94\x2b\x55\xc9\x9f\x56\x2b\x5c\x51\x67\xdc\x32\xe3\xe5\x6a\xee\x5a\xd8\xb7\x37\x43\xea\x86\x3f\xa0\xeb\x24\xae\x1f\x0a\xe4\x94\xc0\xf0\x87\xf4\x93\x41\xd6\xcc\x10\xcb\xa6\xb1\xa9\x2a\x64\x34\xb4\x0a\x02\x2b\x2d\x3c\x1e\x49\x79\xdb\xbf\x48\xb8\xbf\xbe\x9b\x0c\xa1\xc2\xbb\x05\x53\x17\x0d\x72\x2c\x2f\x54\x21\x47\xee\x19\x18\x9a\x3e\x61\x69\xe6\xf8\x7d\xf9\x91\x0f\xb4\xd2\xec\xa4\xb6\xba\x30\x9f\x24\xa3\xe8\x94\xe2\x64\xdd\x3d\xd0\xea\x5f\x46\xf0\x85\x84\x4d\xdc\x3e\x79\x2d\x2a\x70\xa6\xe8\x23\x56\x10\x0f\xc2\x03\x12\xa8\x4c\x8d\x16\x9c\xa0\xe0\xb3\xbc\x34\x62\xd3\x51\xf7\x1f\x82\x01\x02\x3b\x5c\x17\xaa\x8d\x92\xe7\x92\xe0\x14\x4f\xb2\x70\x65\xe5\x5e\xc8\x42\x1f\xfe\xb6\x56\x8a\xb3\xe0\xf2\x18\x00\x43\xb6\x39\x92\xb7\x56\x04\x2b\xfb\x55\xb3\xa2\x61\x92\xd8\xf5\x55\xd7\x3c\xc3\xc5\x47\x5a\xc0\x44\xae\x58\x53\xa4\x3f\x11\xce\x10\x02\x05\x28\xb8\x70\xf3\xdb\x30\xb9\x16\x85\x71\xc8\xee\x06\xd7\xca\xca\xb8\x4e\xed\xe3\xfb\xb7\xaf\xa6\xd7\xc8\x28\xf6\xa3\xec\xd0\x19\x77\xfb\xd6\xab\x1d\x17\xa5\x7d\x69\xdb\x9b\x27\xff\xe0\xfe\xfb\x02\x67\x3f\x21\x2a\x71\x51\xa0\x2d\x96\x34\x73\xf0\x50\xc9\x09\x2e\x46\xae\xf5\xe9\xc4\xa3\x7b\x57\xae\xcd\x4d\x94\xc0\x32\x0f\x13\x93\x38\xca\xd4\x5e\x02\xa3\xaa\x00\x73\xa5\xae\xb1\xb5\x33\x63\xf7\x7a\xdf\xba\x3a\x41\x9b\x1d\x90\x80\xcc\x9a\x80\xcd\x8f\x39\xb8\x18\xe8\x34\x0c\xcc\xd4\x5e\xc1\x93\xee\xc2\xa3\x55\x19\x68\xec\x38\x37\x45\x2d\x2d\xf3\xbf\x00\x00\x00\xff\xff\xf4\xb1\xd8\x9e\x30\x29\x00\x00" +var _adminAuthEditTmpl = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x5a\x6d\x6f\xe3\xb8\x11\xfe\xec\xfb\x15\xac\x7a\x2d\x76\x81\x8b\x8d\xa2\x87\x43\x71\xb0\x03\xe4\x2e\xbb\xb7\x0b\x6c\x0e\x41\x93\xed\x57\x81\x16\xc7\x16\x1b\x89\x54\x49\x2a\xd9\xc0\xf5\x7f\x2f\xf8\x26\x89\x7a\xa1\xe5\x4d\x16\xbd\x2f\x89\x44\xce\x90\xf3\x3c\x24\x47\x33\x63\x1e\x0e\x0a\xca\xaa\xc0\x0a\x50\xb2\xc5\x12\x56\x39\x60\x92\xa0\xe5\xf1\xf8\xdd\x9a\xd0\x47\x94\x15\x58\xca\x4d\x82\x49\x49\x19\x02\x42\x15\xc2\xb5\xca\x81\x29\x9a\x61\x45\x39\x4b\x2e\xbf\x5b\x74\x05\x6b\x8a\x32\xce\x14\xa6\x0c\x84\xee\xeb\x77\xee\x05\x25\xa6\x7d\xd1\x9d\xd9\x0c\xbf\x62\xf8\x71\x8b\x85\x9d\x7c\x11\x6a\xaa\x27\x28\x1e\x01\x3d\x51\x02\x28\xe3\x45\x5d\x32\x33\x0d\x30\x65\x07\x5b\x0c\x70\xe0\x02\x84\x6a\xc6\x5a\xac\xf3\x1f\x3b\x56\x28\x5e\x21\xac\x14\xce\x72\x20\x48\x23\x76\xc6\x9a\x81\x96\xf4\x6f\xff\x60\xcb\x7b\xe1\xcc\x5a\x6a\xc0\x72\xa9\xb1\x27\x7e\xb0\x55\xfe\xa3\x15\xef\xa1\x6b\xc6\x94\xb0\x2f\x5b\xe3\x16\xeb\x1d\x17\x65\x47\x4e\xbf\x26\x08\x67\x9a\xc1\x4d\x72\x38\x2c\x3f\x51\xf6\x70\x3c\x26\xa8\x04\x95\x73\xb2\x49\x2a\x2e\x1b\x65\x6d\xd2\xaf\x77\xff\x7c\x7f\xcf\x1f\x80\x7d\xb8\xbf\xf9\xe4\xac\x58\x2c\xd6\x94\x55\xb5\x42\xea\xb9\x82\x4d\x92\x53\x42\x80\x25\x88\xe1\x12\x36\x09\x25\x09\x7a\xc4\x45\x0d\x66\xf8\x3b\x5e\x8b\x0c\x96\x1f\xaf\x8f\xc7\x66\xd4\xae\xe9\x94\x15\x94\x01\xda\x51\x28\x48\x23\xb0\x58\x17\x78\x0b\xc5\xe5\xe1\xf0\xfd\x38\x25\xfa\x6f\xaa\x27\x4f\x8e\xc7\xf5\xca\x0a\x37\xba\x63\xa6\x51\xb2\x49\x5a\x25\x67\xa9\x7d\x1e\xd8\x7a\xff\x5c\x41\xc7\xda\xc5\x5a\x56\x98\x5d\x86\xfd\xbf\xe3\x12\xf4\xd4\xa6\xcb\xc3\x5a\x11\xfa\x38\x86\x51\xc0\x7f\x6a\x2a\x80\xa0\x2e\x58\x74\x38\xd0\x1d\x5a\xbe\x13\x22\xb5\x83\x81\x10\x5c\x1c\x0e\xc0\x48\x30\xb9\x01\xa7\x97\x6d\x93\x68\xab\x93\xcb\xa9\x6d\x62\xe0\x19\x91\x49\x4e\x34\x0b\x46\xc2\x11\x60\x9f\x07\x04\x58\x7b\x12\x7d\xde\xf8\x8e\x67\xb5\x44\x1e\x41\x0f\xaa\x7f\xfb\xd3\xc5\x05\xfa\x74\x7d\x75\x8b\x30\x23\xe8\xda\x3c\x5d\x5c\xb4\x7b\x88\xee\x10\x17\xa8\xd9\x0a\xd2\x08\xb4\xaf\x46\xa1\xd9\x59\x8b\xc3\x01\x7d\x9f\xed\xf6\x3f\x6f\xbc\x84\x11\x6f\xfb\x47\x76\x4f\x43\x70\x9f\xd9\x3b\xc8\x6a\x41\xd5\xf3\xad\xe0\x8a\x67\xbc\x98\x62\xb9\xdd\x70\xe3\xdc\x4a\x37\x4e\x5a\xb9\x81\x46\x38\xee\x9f\x48\x09\x05\x98\x63\x86\xbc\xf6\x85\xd7\x46\x44\xf0\x8a\xf0\x27\xd6\xb1\x60\x7a\xdf\x0e\x27\x77\xcb\x37\xd2\xd1\xac\xa5\xa6\x70\x39\x84\x1f\x4c\xd8\xf5\x72\xf0\x45\xe9\xad\x35\xaa\xe6\x37\x7b\x67\x7b\x1b\x7b\xbd\xb2\x47\x83\x68\xa6\xdd\xf2\x7a\x45\x27\x66\x29\x81\xd5\x5d\x0b\x16\x87\x83\xc0\x6c\x0f\x68\x30\xa5\x6c\xd7\x7b\xb0\xe6\x0a\xca\x04\x11\xac\xf0\x45\xbb\x73\xfd\x99\x3d\x1c\x96\xe3\xd6\x2e\xdc\x9a\x77\x0c\x0b\x25\xc2\xd7\xde\xdb\xd8\x61\xee\xb9\xac\xe0\xa4\xe6\xc6\x89\x4e\xed\x26\xd3\x3b\xb6\x81\xda\x53\x6a\x44\xdc\x32\xdb\xe7\x70\x65\x3f\x70\xa9\xf4\x11\xad\x0a\x9c\x41\xce\x0b\x02\x62\x93\xc0\x72\xbf\x44\xe5\x33\xe1\x25\xa6\x6c\x99\xf1\x32\x19\x1c\xdc\x97\x02\xab\xb8\x88\x00\x33\xbd\x71\x60\x46\xc4\x01\xb3\xcf\x21\xb0\x5b\x2e\x0c\xb0\x11\x64\x3f\xfd\xfd\xa7\x93\x80\xec\xd1\x0f\x3c\x4d\x67\xcd\xbb\x70\xfb\x28\x03\x98\x5b\xca\x48\x4a\xd8\x34\x52\x2f\x30\x02\xb6\x8b\xd6\x8b\x39\xc0\xcd\x6b\x88\xf9\x17\xca\xc8\xf5\xef\xe3\xcb\x99\xb1\xcd\x1d\x60\x91\xe5\x3f\x90\x6c\xe3\xd7\x56\x3f\xeb\xe5\xed\x4e\x5a\x79\x60\x39\x14\x15\xd2\xe7\x19\x09\x20\x27\x11\xa4\x5a\x1c\x84\x01\x52\x4d\x9d\x06\x0f\xc9\x53\x87\x1f\x20\x71\x8e\xaa\xc2\x52\x3e\x71\x11\xec\x97\xb3\x58\x6e\x07\x88\x5a\xda\x88\xcd\x61\xbc\x11\xee\xf2\xde\x36\xf6\x2c\x1f\x59\x8d\x5b\xd7\xd7\x73\x98\x5f\xc5\xb1\x9f\x67\x26\xd3\x73\x4e\x64\xc0\x62\x2d\x41\xa4\x3a\xf0\x9c\xb6\xa3\x15\x39\xc1\x5e\x2b\xe8\x98\xeb\x34\x84\x2c\x7d\x96\x20\x7e\xc1\x12\xc6\x77\x2d\xaf\x37\x5a\x40\x8e\x6d\xda\xe1\x11\x1e\x9c\xe1\xc0\x53\xf7\x8e\xf4\x75\xe4\x4c\x9f\x41\x58\xec\x70\x7b\x81\x39\x64\xb5\x87\xbb\x79\x1d\x12\x35\x75\xb8\x6b\x4a\x36\x7f\x91\x3f\xbc\x26\x5b\x67\x7b\xf4\x1d\x2d\x94\xce\x41\xa6\xc8\x70\xfd\x71\xaf\xee\x84\x1c\x13\xfe\x2d\x24\xe2\xbd\x69\x1d\x27\xe2\xcd\x5f\xdf\xf0\xed\xbf\x21\x53\xbf\x1a\xcb\x2b\x2e\xe9\x97\xab\x2c\xe3\x35\x53\x6f\xdf\x58\x96\xde\xbe\x3d\xeb\x6b\x16\x83\x6c\xe0\xa5\xa7\x80\x07\x52\x71\xf8\x81\xa8\x23\x21\x6c\x0b\xa9\xb8\xd2\x7d\x0d\x1f\x2f\x45\xa3\x94\xa0\xdb\x5a\x41\xaa\x77\xe0\x89\x1c\x61\x28\x7b\x02\xd9\x50\xc1\xe3\x1b\xe9\xe9\xa1\xf4\x12\x9f\x9d\xc0\x60\xed\xe7\x9b\x99\x76\xf4\x92\xd7\xe4\x6c\x2e\x5f\x67\x71\x35\xce\x53\x94\x23\x97\x73\xbd\x1a\x2e\x59\xcf\xde\x0a\x5e\x74\x2e\x3a\x2f\x3f\x00\xd8\x74\x4c\x60\xbc\xab\xfd\x36\x78\xad\x70\xb4\x9d\xbb\xc4\xb4\x98\x03\xd6\xc8\xcd\x45\x6a\x84\x07\x30\x6d\xeb\x04\xc6\x1b\x4c\x8b\x89\xc0\xdc\xa8\x4d\xb9\xb0\x48\x76\x3b\x20\xa0\x57\xf4\xca\x21\x7b\xd8\xf2\x2f\xc3\xcf\xdd\xe5\x5a\x2a\xc1\xd9\x7e\x92\x96\x47\x10\x74\xf7\x9c\xee\x05\xaf\xab\xb4\x84\x72\x0b\x42\xe6\xb4\x32\xfc\x38\xd5\xc9\x2f\xa0\x33\x00\x18\xde\x16\x70\x21\x9f\xa5\x49\xcb\x6c\x68\xd5\x98\xe4\xb8\xb3\x13\x58\x51\xe2\x72\x37\x85\xc5\x1e\xd4\x26\xf9\xb3\xed\x34\xd2\xe6\x53\x6f\xf8\xfc\x4d\x37\xbe\xb3\x0a\xc7\xa3\x19\x0f\x88\xfb\xd4\x9d\x9f\xb3\x99\x6c\xdc\x94\xc5\xba\x15\x02\xc6\xd5\xd8\x64\x84\x4a\xf3\xe4\x8b\x04\x66\x4f\xb4\x46\x7e\x45\x9c\x6b\x95\x63\x11\x87\x95\x90\x26\xd0\x37\xe1\xd6\x8c\xf0\xa3\x19\x35\x20\x79\x18\x80\x18\x74\x53\x11\x08\xaf\x37\x46\x2f\x9e\x5d\x44\x62\xd4\x19\xc0\x4f\x7d\x68\x03\xa9\x59\xa0\xc3\x2f\x6d\xd8\x36\x02\x3e\x1a\x79\xfc\xf7\x4d\xc6\x36\x7b\xbe\x97\xe6\x5b\x23\xdf\xea\x57\x63\x9d\x8e\x38\x5e\x8b\x02\x7b\xb6\xd2\x9a\x46\x92\x05\x2b\xd9\xba\x19\x57\xcc\x36\x66\xa5\x05\x1d\x2f\x19\x0c\xb9\xe9\x4c\x15\xf0\xd3\x6d\x1f\xe1\xe8\xc6\x74\x7f\x36\x75\xda\x31\xe7\x65\xbb\x29\x79\x05\x4a\x0c\xa0\x28\x15\x46\xa2\x65\x42\x83\x07\x92\x52\x66\x3d\xd5\xac\xb8\xbc\x83\xbf\x7d\x1f\x46\xe6\x93\x80\xeb\x08\xd4\xaf\x2d\x39\x4c\xf8\xf3\x39\x0e\x7d\xae\x47\x6f\x48\x93\x9a\x2e\x9d\x7f\xc6\x9d\xb9\xe7\xad\xf7\x8d\x6b\xb5\x07\x1e\xbd\xf5\xd1\xcd\x37\x4f\x7e\x64\x3a\x69\x9e\xf6\xd3\xf1\x5a\x5b\x98\xc1\xf8\xb7\x6e\x6d\xf9\xee\xe6\x7e\x50\x51\x6e\x19\xd7\xbd\x91\xf2\xb1\x51\x3e\xa3\x7c\x7c\x46\x55\xb8\x54\x55\xaa\x1f\xcf\xaa\x06\x6b\x42\xcf\x2c\x00\x37\xf3\xf8\xc2\x6f\xdb\xd0\x0b\x43\x6a\x95\xeb\xfd\x3c\x4c\x1a\xa7\xab\xbd\x56\xe7\xdb\x56\x77\x6f\xee\x6f\xf5\x34\xe7\x56\x75\x5d\x45\xf7\xff\x5d\xcd\x35\x74\xc7\x4b\xba\x5a\x64\x46\x59\xb7\x1d\xa9\xbb\x92\xb1\x02\xef\x6b\x17\x70\xcd\x84\xf1\x2a\xae\x16\x99\x51\xc9\x6d\x47\xea\x62\x89\xd5\x74\x5f\x2b\x7d\x2f\x0a\xfe\x04\x24\xb5\xd1\x8a\x8c\x44\xfd\x3d\xc1\x13\x61\x7f\x4f\xda\xfb\xc4\x7e\x73\xef\xc4\xd9\xee\x6b\xdb\x1b\xfe\xa6\x14\x94\x0b\x67\x9b\x39\x5e\x27\x0c\xa8\x1a\xf3\x91\xb7\x57\x37\xd3\x2e\xf2\xf6\xea\x26\xe2\x21\xb5\xea\x0b\x2a\x48\x15\x2e\x53\x09\xe2\x91\x66\xa7\xd2\xe9\x81\xe4\x89\xdf\x0a\xfa\xe2\xfe\x77\x83\x41\x7b\xff\x67\x2f\xd3\xe7\x7f\xc6\x8c\x6f\xbb\x31\x2e\x7f\xa3\xea\x43\xbd\x9d\xa6\xd3\xf6\x47\x18\x75\x03\xbc\x80\xd4\x3d\x55\x79\xbd\x4d\x71\x45\x53\x60\xa4\xe2\x94\x45\x4e\xec\x98\x70\x9c\xda\x31\x0d\x1f\x2e\x8e\x75\xf5\x36\xfd\xed\xc7\x77\xae\x6b\x3c\x7a\xca\x95\xaa\xe4\xcf\xab\x15\xae\xa8\x33\x6e\x99\xf1\x72\x75\xee\x5a\x4c\xc4\x4e\x6d\xfa\xd6\x0f\x02\x72\x4a\x60\xf8\x13\xfa\xc9\xf0\x6a\x66\x70\x65\x13\xd8\x54\x15\x32\x1a\x54\x05\x21\x95\x16\x1e\x8f\xa1\xbc\xed\x9f\x25\xdc\x7f\xba\x9b\x0c\x9e\xc2\x5b\x05\x53\x57\x0c\x72\x2c\x2f\x54\x21\x47\x6e\x18\x18\x9a\x3e\x60\x69\xe6\xf8\xb6\xfc\xc8\x07\x5a\x69\x76\x52\x5b\x57\x98\x4f\x92\x51\x74\x4a\x71\xb2\xee\x1e\x68\xf5\x2f\x23\xf8\x42\xc2\x26\xee\x9d\xbc\x16\x15\x38\x53\xf4\x11\x2b\x88\x87\xdf\x01\x09\x54\xa6\x46\x0b\x4e\x50\xf0\x51\x5e\x19\xb1\xe9\x78\xfb\x0f\xc1\x00\x81\x1d\xae\x0b\xd5\xc6\xc7\x73\x49\x70\x8a\x27\x59\xb8\xb6\x72\x2f\x64\xa1\x0f\x7f\x5b\x2b\xc5\x59\x70\x6d\x0c\x80\x21\xdb\x1c\xc9\x58\x2b\x82\x95\xfd\x9e\x59\xd1\x30\x3d\xec\xfa\xaa\xf7\xb4\x80\x89\x04\xb1\xa6\x48\x7f\x17\x9c\x0d\x04\x0a\x50\x70\xe1\xa6\xb6\xb1\x71\x2d\x0a\xe3\x85\xdd\xb5\xad\x95\x95\x71\x9d\xda\xb1\xf7\xaf\x5c\x4d\x2f\x8f\x51\xec\x87\xd6\x61\x5c\xdd\xed\x5b\xaf\x76\x5c\x94\xf6\xa5\x6d\x6f\x9e\xfc\x83\xfb\xef\xab\x9a\xfd\x2c\xa8\xc4\x45\x81\xb6\x58\xd2\xcc\xc1\x43\x25\x27\xb8\x18\xb9\xcb\xa7\xb3\x8d\xee\x05\xb9\x36\x21\x51\x02\xcb\x3c\xcc\x46\xe2\x28\x53\x7b\xf3\x8b\xaa\x02\xcc\x3d\xba\xc6\xd6\xce\x8c\xdd\x3b\x7d\xeb\xea\x04\x6d\x76\x40\x02\x32\x6b\xa2\x34\x3f\xe6\xe0\x36\xa0\xd3\x30\x30\x53\x7b\xef\x4e\xba\x5b\x8e\x56\x65\xa0\xb1\xe3\xdc\x54\xb2\xb4\xcc\xff\x02\x00\x00\xff\xff\xac\x5c\xb2\x0b\x25\x29\x00\x00" func adminAuthEditTmplBytes() ([]byte, error) { return bindataRead( @@ -232,8 +232,8 @@ func adminAuthEditTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "admin/auth/edit.tmpl", size: 10544, mode: os.FileMode(0644), modTime: time.Unix(1571173927, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa5, 0xa0, 0xaa, 0x22, 0x7a, 0x97, 0x4a, 0x99, 0xff, 0xbb, 0x3c, 0x8, 0xc9, 0x28, 0xc4, 0x98, 0xdd, 0x74, 0xff, 0x30, 0xd6, 0x60, 0x2c, 0x39, 0x7c, 0xc8, 0x1d, 0x1, 0xa, 0x24, 0xaf, 0x80}} + info := bindataFileInfo{name: "admin/auth/edit.tmpl", size: 10533, mode: os.FileMode(0644), modTime: time.Unix(1586576842, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x3a, 0x7e, 0x85, 0x71, 0x56, 0xd, 0x56, 0xd7, 0x5d, 0x60, 0xf4, 0xa5, 0xf, 0xcb, 0xf, 0xe4, 0x61, 0xe5, 0x35, 0xfd, 0x45, 0x44, 0xd7, 0x3b, 0x77, 0xd3, 0x38, 0x53, 0xd8, 0x8b, 0x2c, 0xa6}} return a, nil } @@ -1432,7 +1432,7 @@ func repoDiffBoxTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "repo/diff/box.tmpl", size: 6683, mode: os.FileMode(0644), modTime: time.Unix(1585600811, 0)} + info := bindataFileInfo{name: "repo/diff/box.tmpl", size: 6683, mode: os.FileMode(0644), modTime: time.Unix(1585851305, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xfa, 0xbf, 0xac, 0xd5, 0xea, 0x25, 0x2, 0xe9, 0x98, 0xb1, 0xc6, 0x1e, 0x41, 0x64, 0xc8, 0xf0, 0x9d, 0x13, 0xdc, 0xbd, 0x7d, 0x5e, 0xd9, 0x81, 0xf6, 0x93, 0xeb, 0x17, 0xe0, 0x98, 0xd, 0x6}} return a, nil } @@ -2172,7 +2172,7 @@ func repoSettingsOptionsTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "repo/settings/options.tmpl", size: 18622, mode: os.FileMode(0644), modTime: time.Unix(1586374078, 0)} + info := bindataFileInfo{name: "repo/settings/options.tmpl", size: 18622, mode: os.FileMode(0644), modTime: time.Unix(1586528224, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x3b, 0x2f, 0x8a, 0x1c, 0xa4, 0xd9, 0xe4, 0x32, 0xaf, 0x11, 0xc2, 0xec, 0xfa, 0x30, 0xe0, 0x67, 0xa1, 0xb9, 0xba, 0x39, 0x88, 0x85, 0x40, 0x38, 0xbf, 0x1a, 0x90, 0xb4, 0xa4, 0xc4, 0xff, 0xa5}} return a, nil } diff --git a/internal/auth/ldap/ldap.go b/internal/auth/ldap/ldap.go index 29d95560c..d8f8458ed 100644 --- a/internal/auth/ldap/ldap.go +++ b/internal/auth/ldap/ldap.go @@ -19,9 +19,9 @@ type SecurityProtocol int // Note: new type must be added at the end of list to maintain compatibility. const ( - SECURITY_PROTOCOL_UNENCRYPTED SecurityProtocol = iota - SECURITY_PROTOCOL_LDAPS - SECURITY_PROTOCOL_START_TLS + SecurityProtocolUnencrypted SecurityProtocol = iota + SecurityProtocolLDAPS + SecurityProtocolStartTLS ) // Basic LDAP authentication service @@ -144,7 +144,7 @@ func dial(ls *Source) (*ldap.Conn, error) { ServerName: ls.Host, InsecureSkipVerify: ls.SkipVerify, } - if ls.SecurityProtocol == SECURITY_PROTOCOL_LDAPS { + if ls.SecurityProtocol == SecurityProtocolLDAPS { return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), tlsCfg) } @@ -153,7 +153,7 @@ func dial(ls *Source) (*ldap.Conn, error) { return nil, fmt.Errorf("Dial: %v", err) } - if ls.SecurityProtocol == SECURITY_PROTOCOL_START_TLS { + if ls.SecurityProtocol == SecurityProtocolStartTLS { if err = conn.StartTLS(tlsCfg); err != nil { conn.Close() return nil, fmt.Errorf("StartTLS: %v", err) diff --git a/internal/db/access_tokens_test.go b/internal/db/access_tokens_test.go index d3dfb83f7..0f979e95f 100644 --- a/internal/db/access_tokens_test.go +++ b/internal/db/access_tokens_test.go @@ -21,8 +21,9 @@ func Test_accessTokens(t *testing.T) { t.Parallel() + tables := []interface{}{new(AccessToken)} db := &accessTokens{ - DB: initTestDB(t, "accessTokens", new(AccessToken)), + DB: initTestDB(t, "accessTokens", tables...), } for _, tc := range []struct { @@ -37,7 +38,7 @@ func Test_accessTokens(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { - err := deleteTables(db.DB, new(AccessToken)) + err := clearTables(db.DB, tables...) if err != nil { t.Fatal(err) } @@ -78,14 +79,14 @@ func test_accessTokens_DeleteByID(t *testing.T, db *accessTokens) { t.Fatal(err) } - // We should be able to get it back - _, err = db.GetBySHA(token.Sha1) + // Delete a token with mismatched user ID is noop + err = db.DeleteByID(2, token.ID) if err != nil { t.Fatal(err) } - // Delete a token with mismatched user ID is noop - err = db.DeleteByID(2, token.ID) + // We should be able to get it back + _, err = db.GetBySHA(token.Sha1) if err != nil { t.Fatal(err) } diff --git a/internal/db/db.go b/internal/db/db.go index 1be2cc4b5..77d78f536 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -124,7 +124,7 @@ func getLogWriter() (io.Writer, error) { var tables = []interface{}{ new(AccessToken), - new(LFSObject), + new(LFSObject), new(LoginSource), } func Init() error { @@ -167,9 +167,14 @@ func Init() error { return time.Now().UTC().Truncate(time.Microsecond) } + sourceFiles, err := loadLoginSourceFiles(filepath.Join(conf.CustomDir(), "conf", "auth.d")) + if err != nil { + return errors.Wrap(err, "load login source files") + } + // Initialize stores, sorted in alphabetical order. AccessTokens = &accessTokens{DB: db} - LoginSources = &loginSources{DB: db} + LoginSources = &loginSources{DB: db, files: sourceFiles} LFS = &lfs{DB: db} Perms = &perms{DB: db} Repos = &repos{DB: db} diff --git a/internal/db/error.go b/internal/db/error.go index ed173d86c..46e7dde51 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -327,39 +327,6 @@ func (err ErrRepoFileAlreadyExist) Error() string { return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName) } -// .____ .__ _________ -// | | ____ ____ |__| ____ / _____/ ____ __ _________ ____ ____ -// | | / _ \ / ___\| |/ \ \_____ \ / _ \| | \_ __ \_/ ___\/ __ \ -// | |__( <_> ) /_/ > | | \ / ( <_> ) | /| | \/\ \__\ ___/ -// |_______ \____/\___ /|__|___| / /_______ /\____/|____/ |__| \___ >___ > -// \/ /_____/ \/ \/ \/ \/ - -type ErrLoginSourceAlreadyExist struct { - Name string -} - -func IsErrLoginSourceAlreadyExist(err error) bool { - _, ok := err.(ErrLoginSourceAlreadyExist) - return ok -} - -func (err ErrLoginSourceAlreadyExist) Error() string { - return fmt.Sprintf("login source already exists [name: %s]", err.Name) -} - -type ErrLoginSourceInUse struct { - ID int64 -} - -func IsErrLoginSourceInUse(err error) bool { - _, ok := err.(ErrLoginSourceInUse) - return ok -} - -func (err ErrLoginSourceInUse) Error() string { - return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID) -} - // ___________ // \__ ___/___ _____ _____ // | |_/ __ \\__ \ / \ diff --git a/internal/db/errors/login_source.go b/internal/db/errors/login_source.go index 876a08202..db0cd1f9d 100644 --- a/internal/db/errors/login_source.go +++ b/internal/db/errors/login_source.go @@ -6,19 +6,6 @@ package errors import "fmt" -type LoginSourceNotExist struct { - ID int64 -} - -func IsLoginSourceNotExist(err error) bool { - _, ok := err.(LoginSourceNotExist) - return ok -} - -func (err LoginSourceNotExist) Error() string { - return fmt.Sprintf("login source does not exist [id: %d]", err.ID) -} - type LoginSourceNotActivated struct { SourceID int64 } @@ -44,4 +31,3 @@ func IsInvalidLoginSourceType(err error) bool { func (err InvalidLoginSourceType) Error() string { return fmt.Sprintf("invalid login source type [type: %v]", err.Type) } - diff --git a/internal/db/lfs_test.go b/internal/db/lfs_test.go index 6eb14019d..29c7d6659 100644 --- a/internal/db/lfs_test.go +++ b/internal/db/lfs_test.go @@ -22,8 +22,9 @@ func Test_lfs(t *testing.T) { t.Parallel() + tables := []interface{}{new(LFSObject)} db := &lfs{ - DB: initTestDB(t, "lfs", new(LFSObject)), + DB: initTestDB(t, "lfs", tables...), } for _, tc := range []struct { @@ -36,7 +37,7 @@ func Test_lfs(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { - err := deleteTables(db.DB, new(LFSObject)) + err := clearTables(db.DB, tables...) if err != nil { t.Fatal(err) } diff --git a/internal/db/login_source.go b/internal/db/login_source.go index b6665f4ee..e821e1869 100644 --- a/internal/db/login_source.go +++ b/internal/db/login_source.go @@ -10,30 +10,21 @@ import ( "fmt" "net/smtp" "net/textproto" - "os" - "path/filepath" "strings" - "sync" - "time" "github.com/go-macaron/binding" - "github.com/json-iterator/go" "github.com/unknwon/com" - "gopkg.in/ini.v1" - log "unknwon.dev/clog/v2" - "xorm.io/core" - "xorm.io/xorm" "gogs.io/gogs/internal/auth/github" "gogs.io/gogs/internal/auth/ldap" "gogs.io/gogs/internal/auth/pam" - "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/db/errors" ) type LoginType int // Note: new type must append to the end of list to maintain compatibility. +// TODO: Move to authutil. const ( LoginNotype LoginType = iota LoginPlain // 1 @@ -52,497 +43,24 @@ var LoginNames = map[LoginType]string{ LoginGitHub: "GitHub", } -var SecurityProtocolNames = map[ldap.SecurityProtocol]string{ - ldap.SECURITY_PROTOCOL_UNENCRYPTED: "Unencrypted", - ldap.SECURITY_PROTOCOL_LDAPS: "LDAPS", - ldap.SECURITY_PROTOCOL_START_TLS: "StartTLS", -} - -// Ensure structs implemented interface. -var ( - _ core.Conversion = &LDAPConfig{} - _ core.Conversion = &SMTPConfig{} - _ core.Conversion = &PAMConfig{} - _ core.Conversion = &GitHubConfig{} -) +// *********************** +// ----- LDAP config ----- +// *********************** type LDAPConfig struct { - *ldap.Source `ini:"config"` + ldap.Source `ini:"config"` } -func (cfg *LDAPConfig) FromDB(bs []byte) error { - return jsoniter.Unmarshal(bs, &cfg) -} - -func (cfg *LDAPConfig) ToDB() ([]byte, error) { - return jsoniter.Marshal(cfg) +var SecurityProtocolNames = map[ldap.SecurityProtocol]string{ + ldap.SecurityProtocolUnencrypted: "Unencrypted", + ldap.SecurityProtocolLDAPS: "LDAPS", + ldap.SecurityProtocolStartTLS: "StartTLS", } func (cfg *LDAPConfig) SecurityProtocolName() string { return SecurityProtocolNames[cfg.SecurityProtocol] } -type SMTPConfig struct { - Auth string - Host string - Port int - AllowedDomains string `xorm:"TEXT"` - TLS bool `ini:"tls"` - SkipVerify bool -} - -func (cfg *SMTPConfig) FromDB(bs []byte) error { - return jsoniter.Unmarshal(bs, cfg) -} - -func (cfg *SMTPConfig) ToDB() ([]byte, error) { - return jsoniter.Marshal(cfg) -} - -type PAMConfig struct { - ServiceName string // PAM service (e.g. system-auth) -} - -func (cfg *PAMConfig) FromDB(bs []byte) error { - return jsoniter.Unmarshal(bs, &cfg) -} - -func (cfg *PAMConfig) ToDB() ([]byte, error) { - return jsoniter.Marshal(cfg) -} - -type GitHubConfig struct { - APIEndpoint string // GitHub service (e.g. https://api.github.com/) -} - -func (cfg *GitHubConfig) FromDB(bs []byte) error { - return jsoniter.Unmarshal(bs, &cfg) -} - -func (cfg *GitHubConfig) ToDB() ([]byte, error) { - return jsoniter.Marshal(cfg) -} - -// AuthSourceFile contains information of an authentication source file. -type AuthSourceFile struct { - abspath string - file *ini.File -} - -// SetGeneral sets new value to the given key in the general (default) section. -func (f *AuthSourceFile) SetGeneral(name, value string) { - f.file.Section("").Key(name).SetValue(value) -} - -// SetConfig sets new values to the "config" section. -func (f *AuthSourceFile) SetConfig(cfg core.Conversion) error { - return f.file.Section("config").ReflectFrom(cfg) -} - -// Save writes updates into file system. -func (f *AuthSourceFile) Save() error { - return f.file.SaveTo(f.abspath) -} - -// LoginSource represents an external way for authorizing users. -type LoginSource struct { - ID int64 - Type LoginType - Name string `xorm:"UNIQUE"` - IsActived bool `xorm:"NOT NULL DEFAULT false"` - IsDefault bool `xorm:"DEFAULT false"` - Cfg core.Conversion `xorm:"TEXT" gorm:"COLUMN:remove-me-when-migrated-to-gorm"` - RawCfg string `xorm:"-" gorm:"COLUMN:cfg"` // TODO: Remove me when migrated to GORM. - - Created time.Time `xorm:"-" json:"-"` - CreatedUnix int64 - Updated time.Time `xorm:"-" json:"-"` - UpdatedUnix int64 - - LocalFile *AuthSourceFile `xorm:"-" json:"-"` -} - -func (s *LoginSource) BeforeInsert() { - s.CreatedUnix = time.Now().Unix() - s.UpdatedUnix = s.CreatedUnix -} - -func (s *LoginSource) BeforeUpdate() { - s.UpdatedUnix = time.Now().Unix() -} - -// Cell2Int64 converts a xorm.Cell type to int64, -// and handles possible irregular cases. -func Cell2Int64(val xorm.Cell) int64 { - switch (*val).(type) { - case []uint8: - log.Trace("Cell2Int64 ([]uint8): %v", *val) - return com.StrTo(string((*val).([]uint8))).MustInt64() - } - return (*val).(int64) -} - -func (s *LoginSource) BeforeSet(colName string, val xorm.Cell) { - switch colName { - case "type": - switch LoginType(Cell2Int64(val)) { - case LoginLDAP, LoginDLDAP: - s.Cfg = new(LDAPConfig) - case LoginSMTP: - s.Cfg = new(SMTPConfig) - case LoginPAM: - s.Cfg = new(PAMConfig) - case LoginGitHub: - s.Cfg = new(GitHubConfig) - default: - panic("unrecognized login source type: " + com.ToStr(*val)) - } - } -} - -func (s *LoginSource) AfterSet(colName string, _ xorm.Cell) { - switch colName { - case "created_unix": - s.Created = time.Unix(s.CreatedUnix, 0).Local() - case "updated_unix": - s.Updated = time.Unix(s.UpdatedUnix, 0).Local() - } -} - -// NOTE: This is a GORM query hook. -func (s *LoginSource) AfterFind() error { - switch s.Type { - case LoginLDAP, LoginDLDAP: - s.Cfg = new(LDAPConfig) - case LoginSMTP: - s.Cfg = new(SMTPConfig) - case LoginPAM: - s.Cfg = new(PAMConfig) - case LoginGitHub: - s.Cfg = new(GitHubConfig) - default: - return fmt.Errorf("unrecognized login source type: %v", s.Type) - } - return jsoniter.UnmarshalFromString(s.RawCfg, s.Cfg) -} - -func (s *LoginSource) TypeName() string { - return LoginNames[s.Type] -} - -func (s *LoginSource) IsLDAP() bool { - return s.Type == LoginLDAP -} - -func (s *LoginSource) IsDLDAP() bool { - return s.Type == LoginDLDAP -} - -func (s *LoginSource) IsSMTP() bool { - return s.Type == LoginSMTP -} - -func (s *LoginSource) IsPAM() bool { - return s.Type == LoginPAM -} - -func (s *LoginSource) IsGitHub() bool { - return s.Type == LoginGitHub -} - -func (s *LoginSource) HasTLS() bool { - return ((s.IsLDAP() || s.IsDLDAP()) && - s.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) || - s.IsSMTP() -} - -func (s *LoginSource) UseTLS() bool { - switch s.Type { - case LoginLDAP, LoginDLDAP: - return s.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED - case LoginSMTP: - return s.SMTP().TLS - } - - return false -} - -func (s *LoginSource) SkipVerify() bool { - switch s.Type { - case LoginLDAP, LoginDLDAP: - return s.LDAP().SkipVerify - case LoginSMTP: - return s.SMTP().SkipVerify - } - - return false -} - -func (s *LoginSource) LDAP() *LDAPConfig { - return s.Cfg.(*LDAPConfig) -} - -func (s *LoginSource) SMTP() *SMTPConfig { - return s.Cfg.(*SMTPConfig) -} - -func (s *LoginSource) PAM() *PAMConfig { - return s.Cfg.(*PAMConfig) -} - -func (s *LoginSource) GitHub() *GitHubConfig { - return s.Cfg.(*GitHubConfig) -} - -func CreateLoginSource(source *LoginSource) error { - has, err := x.Get(&LoginSource{Name: source.Name}) - if err != nil { - return err - } else if has { - return ErrLoginSourceAlreadyExist{source.Name} - } - - _, err = x.Insert(source) - if err != nil { - return err - } else if source.IsDefault { - return ResetNonDefaultLoginSources(source) - } - return nil -} - -// ListLoginSources returns all login sources defined. -func ListLoginSources() ([]*LoginSource, error) { - sources := make([]*LoginSource, 0, 2) - if err := x.Find(&sources); err != nil { - return nil, err - } - - return append(sources, localLoginSources.List()...), nil -} - -// ActivatedLoginSources returns login sources that are currently activated. -func ActivatedLoginSources() ([]*LoginSource, error) { - sources := make([]*LoginSource, 0, 2) - if err := x.Where("is_actived = ?", true).Find(&sources); err != nil { - return nil, fmt.Errorf("find activated login sources: %v", err) - } - return append(sources, localLoginSources.ActivatedList()...), nil -} - -// ResetNonDefaultLoginSources clean other default source flag -func ResetNonDefaultLoginSources(source *LoginSource) error { - // update changes to DB - if _, err := x.NotIn("id", []int64{source.ID}).Cols("is_default").Update(&LoginSource{IsDefault: false}); err != nil { - return err - } - // write changes to local authentications - for i := range localLoginSources.sources { - if localLoginSources.sources[i].LocalFile != nil && localLoginSources.sources[i].ID != source.ID { - localLoginSources.sources[i].LocalFile.SetGeneral("is_default", "false") - if err := localLoginSources.sources[i].LocalFile.SetConfig(source.Cfg); err != nil { - return fmt.Errorf("LocalFile.SetConfig: %v", err) - } else if err = localLoginSources.sources[i].LocalFile.Save(); err != nil { - return fmt.Errorf("LocalFile.Save: %v", err) - } - } - } - // flush memory so that web page can show the same behaviors - localLoginSources.UpdateLoginSource(source) - return nil -} - -// UpdateLoginSource updates information of login source to database or local file. -func UpdateLoginSource(source *LoginSource) error { - if source.LocalFile == nil { - if _, err := x.Id(source.ID).AllCols().Update(source); err != nil { - return err - } else { - return ResetNonDefaultLoginSources(source) - } - - } - - source.LocalFile.SetGeneral("name", source.Name) - source.LocalFile.SetGeneral("is_activated", com.ToStr(source.IsActived)) - source.LocalFile.SetGeneral("is_default", com.ToStr(source.IsDefault)) - if err := source.LocalFile.SetConfig(source.Cfg); err != nil { - return fmt.Errorf("LocalFile.SetConfig: %v", err) - } else if err = source.LocalFile.Save(); err != nil { - return fmt.Errorf("LocalFile.Save: %v", err) - } - return ResetNonDefaultLoginSources(source) -} - -func DeleteSource(source *LoginSource) error { - count, err := x.Count(&User{LoginSource: source.ID}) - if err != nil { - return err - } else if count > 0 { - return ErrLoginSourceInUse{source.ID} - } - _, err = x.Id(source.ID).Delete(new(LoginSource)) - return err -} - -// CountLoginSources returns total number of login sources. -func CountLoginSources() int64 { - count, _ := x.Count(new(LoginSource)) - return count + int64(localLoginSources.Len()) -} - -// LocalLoginSources contains authentication sources configured and loaded from local files. -// Calling its methods is thread-safe; otherwise, please maintain the mutex accordingly. -type LocalLoginSources struct { - sync.RWMutex - sources []*LoginSource -} - -func (s *LocalLoginSources) Len() int { - return len(s.sources) -} - -// List returns full clone of login sources. -func (s *LocalLoginSources) List() []*LoginSource { - s.RLock() - defer s.RUnlock() - - list := make([]*LoginSource, s.Len()) - for i := range s.sources { - list[i] = &LoginSource{} - *list[i] = *s.sources[i] - } - return list -} - -// ActivatedList returns clone of activated login sources. -func (s *LocalLoginSources) ActivatedList() []*LoginSource { - s.RLock() - defer s.RUnlock() - - list := make([]*LoginSource, 0, 2) - for i := range s.sources { - if !s.sources[i].IsActived { - continue - } - source := &LoginSource{} - *source = *s.sources[i] - list = append(list, source) - } - return list -} - -// GetLoginSourceByID returns a clone of login source by given ID. -func (s *LocalLoginSources) GetLoginSourceByID(id int64) (*LoginSource, error) { - s.RLock() - defer s.RUnlock() - - for i := range s.sources { - if s.sources[i].ID == id { - source := &LoginSource{} - *source = *s.sources[i] - return source, nil - } - } - - return nil, errors.LoginSourceNotExist{ID: id} -} - -// UpdateLoginSource updates in-memory copy of the authentication source. -func (s *LocalLoginSources) UpdateLoginSource(source *LoginSource) { - s.Lock() - defer s.Unlock() - - source.Updated = time.Now() - for i := range s.sources { - if s.sources[i].ID == source.ID { - *s.sources[i] = *source - } else if source.IsDefault { - s.sources[i].IsDefault = false - } - } -} - -var localLoginSources = &LocalLoginSources{} - -// LoadAuthSources loads authentication sources from local files -// and converts them into login sources. -func LoadAuthSources() { - authdPath := filepath.Join(conf.CustomDir(), "conf", "auth.d") - if !com.IsDir(authdPath) { - return - } - - paths, err := com.GetFileListBySuffix(authdPath, ".conf") - if err != nil { - log.Fatal("Failed to list authentication sources: %v", err) - } - - localLoginSources.sources = make([]*LoginSource, 0, len(paths)) - - for _, fpath := range paths { - authSource, err := ini.Load(fpath) - if err != nil { - log.Fatal("Failed to load authentication source: %v", err) - } - authSource.NameMapper = ini.TitleUnderscore - - // Set general attributes - s := authSource.Section("") - loginSource := &LoginSource{ - ID: s.Key("id").MustInt64(), - Name: s.Key("name").String(), - IsActived: s.Key("is_activated").MustBool(), - IsDefault: s.Key("is_default").MustBool(), - LocalFile: &AuthSourceFile{ - abspath: fpath, - file: authSource, - }, - } - - fi, err := os.Stat(fpath) - if err != nil { - log.Fatal("Failed to load authentication source: %v", err) - } - loginSource.Updated = fi.ModTime() - - // Parse authentication source file - authType := s.Key("type").String() - switch authType { - case "ldap_bind_dn": - loginSource.Type = LoginLDAP - loginSource.Cfg = &LDAPConfig{} - case "ldap_simple_auth": - loginSource.Type = LoginDLDAP - loginSource.Cfg = &LDAPConfig{} - case "smtp": - loginSource.Type = LoginSMTP - loginSource.Cfg = &SMTPConfig{} - case "pam": - loginSource.Type = LoginPAM - loginSource.Cfg = &PAMConfig{} - case "github": - loginSource.Type = LoginGitHub - loginSource.Cfg = &GitHubConfig{} - default: - log.Fatal("Failed to load authentication source: unknown type '%s'", authType) - } - - if err = authSource.Section("config").MapTo(loginSource.Cfg); err != nil { - log.Fatal("Failed to parse authentication source 'config': %v", err) - } - - localLoginSources.sources = append(localLoginSources.sources, loginSource) - } -} - -// .____ ________ _____ __________ -// | | \______ \ / _ \\______ \ -// | | | | \ / /_\ \| ___/ -// | |___ | ` \/ | \ | -// |_______ \/_______ /\____|__ /____| -// \/ \/ \/ - func composeFullName(firstname, surname, username string) string { switch { case len(firstname) == 0 && len(surname) == 0: @@ -559,7 +77,7 @@ func composeFullName(firstname, surname, username string) string { // LoginViaLDAP queries if login/password is valid against the LDAP directory pool, // and create a local user if success when enabled. func LoginViaLDAP(login, password string, source *LoginSource, autoRegister bool) (*User, error) { - username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP) + username, fn, sn, mail, isAdmin, succeed := source.Config.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP) if !succeed { // User not in LDAP, do nothing return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}} @@ -606,12 +124,18 @@ func LoginViaLDAP(login, password string, source *LoginSource, autoRegister bool return user, CreateUser(user) } -// _________ __________________________ -// / _____/ / \__ ___/\______ \ -// \_____ \ / \ / \| | | ___/ -// / \/ Y \ | | | -// /_______ /\____|__ /____| |____| -// \/ \/ +// *********************** +// ----- SMTP config ----- +// *********************** + +type SMTPConfig struct { + Auth string + Host string + Port int + AllowedDomains string + TLS bool `ini:"tls"` + SkipVerify bool +} type smtpLoginAuth struct { username, password string @@ -634,11 +158,11 @@ func (auth *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) { } const ( - SMTP_PLAIN = "PLAIN" - SMTP_LOGIN = "LOGIN" + SMTPPlain = "PLAIN" + SMTPLogin = "LOGIN" ) -var SMTPAuths = []string{SMTP_PLAIN, SMTP_LOGIN} +var SMTPAuths = []string{SMTPPlain, SMTPLogin} func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error { c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)) @@ -687,9 +211,9 @@ func LoginViaSMTP(login, password string, sourceID int64, cfg *SMTPConfig, autoR } var auth smtp.Auth - if cfg.Auth == SMTP_PLAIN { + if cfg.Auth == SMTPPlain { auth = smtp.PlainAuth("", login, password, cfg.Host) - } else if cfg.Auth == SMTP_LOGIN { + } else if cfg.Auth == SMTPLogin { auth = &smtpLoginAuth{login, password} } else { return nil, errors.New("Unsupported SMTP authentication type") @@ -729,12 +253,14 @@ func LoginViaSMTP(login, password string, sourceID int64, cfg *SMTPConfig, autoR return user, CreateUser(user) } -// __________ _____ _____ -// \______ \/ _ \ / \ -// | ___/ /_\ \ / \ / \ -// | | / | \/ Y \ -// |____| \____|__ /\____|__ / -// \/ \/ +// ********************** +// ----- PAM config ----- +// ********************** + +type PAMConfig struct { + // The name of the PAM service, e.g. system-auth. + ServiceName string +} // LoginViaPAM queries if login/password is valid against the PAM, // and create a local user if success when enabled. @@ -763,12 +289,14 @@ func LoginViaPAM(login, password string, sourceID int64, cfg *PAMConfig, autoReg return user, CreateUser(user) } -// ________.__ __ ___ ___ ___. -// / _____/|__|/ |_ / | \ __ _\_ |__ -// / \ ___| \ __\/ ~ \ | \ __ \ -// \ \_\ \ || | \ Y / | / \_\ \ -// \______ /__||__| \___|_ /|____/|___ / -// \/ \/ \/ +// ************************* +// ----- GitHub config ----- +// ************************* + +type GitHubConfig struct { + // the GitHub service endpoint, e.g. https://api.github.com/. + APIEndpoint string +} func LoginViaGitHub(login, password string, sourceID int64, cfg *GitHubConfig, autoRegister bool) (*User, error) { fullname, email, url, location, err := github.Authenticate(cfg.APIEndpoint, login, password) @@ -807,11 +335,11 @@ func authenticateViaLoginSource(source *LoginSource, login, password string, aut case LoginLDAP, LoginDLDAP: return LoginViaLDAP(login, password, source, autoRegister) case LoginSMTP: - return LoginViaSMTP(login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister) + return LoginViaSMTP(login, password, source.ID, source.Config.(*SMTPConfig), autoRegister) case LoginPAM: - return LoginViaPAM(login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister) + return LoginViaPAM(login, password, source.ID, source.Config.(*PAMConfig), autoRegister) case LoginGitHub: - return LoginViaGitHub(login, password, source.ID, source.Cfg.(*GitHubConfig), autoRegister) + return LoginViaGitHub(login, password, source.ID, source.Config.(*GitHubConfig), autoRegister) } return nil, errors.InvalidLoginSourceType{Type: source.Type} diff --git a/internal/db/login_source_files.go b/internal/db/login_source_files.go new file mode 100644 index 000000000..4c5cb3776 --- /dev/null +++ b/internal/db/login_source_files.go @@ -0,0 +1,212 @@ +// Copyright 2020 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package db + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + "gopkg.in/ini.v1" + + "gogs.io/gogs/internal/errutil" + "gogs.io/gogs/internal/osutil" +) + +// loginSourceFilesStore is the in-memory interface for login source files stored on file system. +// +// NOTE: All methods are sorted in alphabetical order. +type loginSourceFilesStore interface { + // GetByID returns a clone of login source by given ID. + GetByID(id int64) (*LoginSource, error) + // Len returns number of login sources. + Len() int + // List returns a list of login sources filtered by options. + List(opts ListLoginSourceOpts) []*LoginSource + // Update updates in-memory copy of the authentication source. + Update(source *LoginSource) +} + +var _ loginSourceFilesStore = (*loginSourceFiles)(nil) + +// loginSourceFiles contains authentication sources configured and loaded from local files. +type loginSourceFiles struct { + sync.RWMutex + sources []*LoginSource +} + +var _ errutil.NotFound = (*ErrLoginSourceNotExist)(nil) + +type ErrLoginSourceNotExist struct { + args errutil.Args +} + +func IsErrLoginSourceNotExist(err error) bool { + _, ok := err.(ErrLoginSourceNotExist) + return ok +} + +func (err ErrLoginSourceNotExist) Error() string { + return fmt.Sprintf("login source does not exist: %v", err.args) +} + +func (ErrLoginSourceNotExist) NotFound() bool { + return true +} + +func (s *loginSourceFiles) GetByID(id int64) (*LoginSource, error) { + s.RLock() + defer s.RUnlock() + + for _, source := range s.sources { + if source.ID == id { + return source, nil + } + } + + return nil, ErrLoginSourceNotExist{args: errutil.Args{"id": id}} +} + +func (s *loginSourceFiles) Len() int { + s.RLock() + defer s.RUnlock() + return len(s.sources) +} + +func (s *loginSourceFiles) List(opts ListLoginSourceOpts) []*LoginSource { + s.RLock() + defer s.RUnlock() + + list := make([]*LoginSource, 0, s.Len()) + for _, source := range s.sources { + if opts.OnlyActivated && !source.IsActived { + continue + } + + list = append(list, source) + } + return list +} + +func (s *loginSourceFiles) Update(source *LoginSource) { + s.Lock() + defer s.Unlock() + + source.Updated = gorm.NowFunc() + for _, old := range s.sources { + if old.ID == source.ID { + *old = *source + } else if source.IsDefault { + old.IsDefault = false + } + } +} + +// loadLoginSourceFiles loads login sources from file system. +func loadLoginSourceFiles(authdPath string) (loginSourceFilesStore, error) { + if !osutil.IsDir(authdPath) { + return &loginSourceFiles{}, nil + } + + store := &loginSourceFiles{} + return store, filepath.Walk(authdPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if path == authdPath || !strings.HasSuffix(path, ".conf") { + return nil + } else if info.IsDir() { + return filepath.SkipDir + } + + authSource, err := ini.Load(path) + if err != nil { + return errors.Wrap(err, "load file") + } + authSource.NameMapper = ini.TitleUnderscore + + // Set general attributes + s := authSource.Section("") + loginSource := &LoginSource{ + ID: s.Key("id").MustInt64(), + Name: s.Key("name").String(), + IsActived: s.Key("is_activated").MustBool(), + IsDefault: s.Key("is_default").MustBool(), + File: &loginSourceFile{ + path: path, + file: authSource, + }, + } + + fi, err := os.Stat(path) + if err != nil { + return errors.Wrap(err, "stat file") + } + loginSource.Updated = fi.ModTime() + + // Parse authentication source file + authType := s.Key("type").String() + switch authType { + case "ldap_bind_dn": + loginSource.Type = LoginLDAP + loginSource.Config = &LDAPConfig{} + case "ldap_simple_auth": + loginSource.Type = LoginDLDAP + loginSource.Config = &LDAPConfig{} + case "smtp": + loginSource.Type = LoginSMTP + loginSource.Config = &SMTPConfig{} + case "pam": + loginSource.Type = LoginPAM + loginSource.Config = &PAMConfig{} + case "github": + loginSource.Type = LoginGitHub + loginSource.Config = &GitHubConfig{} + default: + return fmt.Errorf("unknown type %q", authType) + } + + if err = authSource.Section("config").MapTo(loginSource.Config); err != nil { + return errors.Wrap(err, `map "config" section`) + } + + store.sources = append(store.sources, loginSource) + return nil + }) +} + +// loginSourceFileStore is the persistent interface for a login source file. +type loginSourceFileStore interface { + // SetGeneral sets new value to the given key in the general (default) section. + SetGeneral(name, value string) + // SetConfig sets new values to the "config" section. + SetConfig(cfg interface{}) error + // Save persists values to file system. + Save() error +} + +var _ loginSourceFileStore = (*loginSourceFile)(nil) + +type loginSourceFile struct { + path string + file *ini.File +} + +func (f *loginSourceFile) SetGeneral(name, value string) { + f.file.Section("").Key(name).SetValue(value) +} + +func (f *loginSourceFile) SetConfig(cfg interface{}) error { + return f.file.Section("config").ReflectFrom(cfg) +} + +func (f *loginSourceFile) Save() error { + return f.file.SaveTo(f.path) +} diff --git a/internal/db/login_sources.go b/internal/db/login_sources.go index 41ee73b95..b620a4426 100644 --- a/internal/db/login_sources.go +++ b/internal/db/login_sources.go @@ -5,22 +5,242 @@ package db import ( + "fmt" + "strconv" + "time" + "github.com/jinzhu/gorm" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + + "gogs.io/gogs/internal/auth/ldap" + "gogs.io/gogs/internal/errutil" ) // LoginSourcesStore is the persistent interface for login sources. // // NOTE: All methods are sorted in alphabetical order. type LoginSourcesStore interface { + // Create creates a new login source and persist to database. + // It returns ErrLoginSourceAlreadyExist when a login source with same name already exists. + Create(opts CreateLoginSourceOpts) (*LoginSource, error) + // Count returns the total number of login sources. + Count() int64 + // DeleteByID deletes a login source by given ID. + // It returns ErrLoginSourceInUse if at least one user is associated with the login source. + DeleteByID(id int64) error // GetByID returns the login source with given ID. // It returns ErrLoginSourceNotExist when not found. GetByID(id int64) (*LoginSource, error) + // List returns a list of login sources filtered by options. + List(opts ListLoginSourceOpts) ([]*LoginSource, error) + // ResetNonDefault clears default flag for all the other login sources. + ResetNonDefault(source *LoginSource) error + // Save persists all values of given login source to database or local file. + // The Updated field is set to current time automatically. + Save(t *LoginSource) error } var LoginSources LoginSourcesStore +// LoginSource represents an external way for authorizing users. +type LoginSource struct { + ID int64 + Type LoginType + Name string `xorm:"UNIQUE"` + IsActived bool `xorm:"NOT NULL DEFAULT false"` + IsDefault bool `xorm:"DEFAULT false"` + Config interface{} `xorm:"-" gorm:"-"` + RawConfig string `xorm:"TEXT cfg" gorm:"COLUMN:cfg"` + + Created time.Time `xorm:"-" gorm:"-" json:"-"` + CreatedUnix int64 + Updated time.Time `xorm:"-" gorm:"-" json:"-"` + UpdatedUnix int64 + + File loginSourceFileStore `xorm:"-" gorm:"-" json:"-"` +} + +// NOTE: This is a GORM save hook. +func (s *LoginSource) BeforeSave() (err error) { + s.RawConfig, err = jsoniter.MarshalToString(s.Config) + return err +} + +// NOTE: This is a GORM create hook. +func (s *LoginSource) BeforeCreate() { + s.CreatedUnix = gorm.NowFunc().Unix() + s.UpdatedUnix = s.CreatedUnix +} + +// NOTE: This is a GORM update hook. +func (s *LoginSource) BeforeUpdate() { + s.UpdatedUnix = gorm.NowFunc().Unix() +} + +// NOTE: This is a GORM query hook. +func (s *LoginSource) AfterFind() error { + s.Created = time.Unix(s.CreatedUnix, 0).Local() + s.Updated = time.Unix(s.UpdatedUnix, 0).Local() + + switch s.Type { + case LoginLDAP, LoginDLDAP: + s.Config = new(LDAPConfig) + case LoginSMTP: + s.Config = new(SMTPConfig) + case LoginPAM: + s.Config = new(PAMConfig) + case LoginGitHub: + s.Config = new(GitHubConfig) + default: + return fmt.Errorf("unrecognized login source type: %v", s.Type) + } + return jsoniter.UnmarshalFromString(s.RawConfig, s.Config) +} + +func (s *LoginSource) TypeName() string { + return LoginNames[s.Type] +} + +func (s *LoginSource) IsLDAP() bool { + return s.Type == LoginLDAP +} + +func (s *LoginSource) IsDLDAP() bool { + return s.Type == LoginDLDAP +} + +func (s *LoginSource) IsSMTP() bool { + return s.Type == LoginSMTP +} + +func (s *LoginSource) IsPAM() bool { + return s.Type == LoginPAM +} + +func (s *LoginSource) IsGitHub() bool { + return s.Type == LoginGitHub +} + +func (s *LoginSource) HasTLS() bool { + return ((s.IsLDAP() || s.IsDLDAP()) && + s.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) || + s.IsSMTP() +} + +func (s *LoginSource) UseTLS() bool { + switch s.Type { + case LoginLDAP, LoginDLDAP: + return s.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted + case LoginSMTP: + return s.SMTP().TLS + } + + return false +} + +func (s *LoginSource) SkipVerify() bool { + switch s.Type { + case LoginLDAP, LoginDLDAP: + return s.LDAP().SkipVerify + case LoginSMTP: + return s.SMTP().SkipVerify + } + + return false +} + +func (s *LoginSource) LDAP() *LDAPConfig { + return s.Config.(*LDAPConfig) +} + +func (s *LoginSource) SMTP() *SMTPConfig { + return s.Config.(*SMTPConfig) +} + +func (s *LoginSource) PAM() *PAMConfig { + return s.Config.(*PAMConfig) +} + +func (s *LoginSource) GitHub() *GitHubConfig { + return s.Config.(*GitHubConfig) +} + +var _ LoginSourcesStore = (*loginSources)(nil) + type loginSources struct { *gorm.DB + files loginSourceFilesStore +} + +type CreateLoginSourceOpts struct { + Type LoginType + Name string + Activated bool + Default bool + Config interface{} +} + +type ErrLoginSourceAlreadyExist struct { + args errutil.Args +} + +func IsErrLoginSourceAlreadyExist(err error) bool { + _, ok := err.(ErrLoginSourceAlreadyExist) + return ok +} + +func (err ErrLoginSourceAlreadyExist) Error() string { + return fmt.Sprintf("login source already exists: %v", err.args) +} + +func (db *loginSources) Create(opts CreateLoginSourceOpts) (*LoginSource, error) { + err := db.Where("name = ?", opts.Name).First(new(LoginSource)).Error + if err == nil { + return nil, ErrLoginSourceAlreadyExist{args: errutil.Args{"name": opts.Name}} + } else if !gorm.IsRecordNotFoundError(err) { + return nil, err + } + + source := &LoginSource{ + Type: opts.Type, + Name: opts.Name, + IsActived: opts.Activated, + IsDefault: opts.Default, + Config: opts.Config, + } + return source, db.DB.Create(source).Error +} + +func (db *loginSources) Count() int64 { + var count int64 + db.Model(new(LoginSource)).Count(&count) + return count + int64(db.files.Len()) +} + +type ErrLoginSourceInUse struct { + args errutil.Args +} + +func IsErrLoginSourceInUse(err error) bool { + _, ok := err.(ErrLoginSourceInUse) + return ok +} + +func (err ErrLoginSourceInUse) Error() string { + return fmt.Sprintf("login source is still used by some users: %v", err.args) +} + +func (db *loginSources) DeleteByID(id int64) error { + var count int64 + err := db.Model(new(User)).Where("login_source = ?", id).Count(&count).Error + if err != nil { + return err + } else if count > 0 { + return ErrLoginSourceInUse{args: errutil.Args{"id": id}} + } + + return db.Where("id = ?", id).Delete(new(LoginSource)).Error } func (db *loginSources) GetByID(id int64) (*LoginSource, error) { @@ -28,9 +248,63 @@ func (db *loginSources) GetByID(id int64) (*LoginSource, error) { err := db.Where("id = ?", id).First(source).Error if err != nil { if gorm.IsRecordNotFoundError(err) { - return localLoginSources.GetLoginSourceByID(id) + return db.files.GetByID(id) } return nil, err } return source, nil } + +type ListLoginSourceOpts struct { + // Whether to only include activated login sources. + OnlyActivated bool +} + +func (db *loginSources) List(opts ListLoginSourceOpts) ([]*LoginSource, error) { + var sources []*LoginSource + query := db.Order("id ASC") + if opts.OnlyActivated { + query = query.Where("is_actived = ?", true) + } + err := query.Find(&sources).Error + if err != nil { + return nil, err + } + + return append(sources, db.files.List(opts)...), nil +} + +func (db *loginSources) ResetNonDefault(dflt *LoginSource) error { + err := db.Model(new(LoginSource)).Where("id != ?", dflt.ID).Updates(map[string]interface{}{"is_default": false}).Error + if err != nil { + return err + } + + for _, source := range db.files.List(ListLoginSourceOpts{}) { + if source.File != nil && source.ID != dflt.ID { + source.File.SetGeneral("is_default", "false") + if err = source.File.Save(); err != nil { + return errors.Wrap(err, "save file") + } + } + } + + db.files.Update(dflt) + return nil +} + +func (db *loginSources) Save(source *LoginSource) error { + if source.File == nil { + return db.DB.Save(source).Error + } + + source.File.SetGeneral("name", source.Name) + source.File.SetGeneral("is_activated", strconv.FormatBool(source.IsActived)) + source.File.SetGeneral("is_default", strconv.FormatBool(source.IsDefault)) + if err := source.File.SetConfig(source.Config); err != nil { + return errors.Wrap(err, "set config") + } else if err = source.File.Save(); err != nil { + return errors.Wrap(err, "save file") + } + return nil +} diff --git a/internal/db/login_sources_test.go b/internal/db/login_sources_test.go new file mode 100644 index 000000000..8cc0ab19c --- /dev/null +++ b/internal/db/login_sources_test.go @@ -0,0 +1,389 @@ +// Copyright 2020 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package db + +import ( + "testing" + "time" + + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + + "gogs.io/gogs/internal/errutil" +) + +func Test_loginSources(t *testing.T) { + if testing.Short() { + t.Skip() + } + + t.Parallel() + + tables := []interface{}{new(LoginSource), new(User)} + db := &loginSources{ + DB: initTestDB(t, "loginSources", tables...), + } + + for _, tc := range []struct { + name string + test func(*testing.T, *loginSources) + }{ + {"Create", test_loginSources_Create}, + {"Count", test_loginSources_Count}, + {"DeleteByID", test_loginSources_DeleteByID}, + {"GetByID", test_loginSources_GetByID}, + {"List", test_loginSources_List}, + {"ResetNonDefault", test_loginSources_ResetNonDefault}, + {"Save", test_loginSources_Save}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Cleanup(func() { + err := clearTables(db.DB, tables...) + if err != nil { + t.Fatal(err) + } + }) + tc.test(t, db) + }) + } +} + +func test_loginSources_Create(t *testing.T, db *loginSources) { + // Create first login source with name "GitHub" + source, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Default: false, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + // Get it back and check the Created field + source, err = db.GetByID(source.ID) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, gorm.NowFunc().Format(time.RFC3339), source.Created.Format(time.RFC3339)) + assert.Equal(t, gorm.NowFunc().Format(time.RFC3339), source.Updated.Format(time.RFC3339)) + + // Try create second login source with same name should fail + _, err = db.Create(CreateLoginSourceOpts{Name: source.Name}) + expErr := ErrLoginSourceAlreadyExist{args: errutil.Args{"name": source.Name}} + assert.Equal(t, expErr, err) +} + +func test_loginSources_Count(t *testing.T, db *loginSources) { + // Create two login sources, one in database and one as source file. + _, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Default: false, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + setMockLoginSourceFilesStore(t, db, &mockLoginSourceFilesStore{ + MockLen: func() int { + return 2 + }, + }) + + assert.Equal(t, int64(3), db.Count()) +} + +func test_loginSources_DeleteByID(t *testing.T, db *loginSources) { + t.Run("delete but in used", func(t *testing.T) { + source, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Default: false, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + // Create a user that uses this login source + user := &User{ + LoginSource: source.ID, + } + err = db.DB.Create(user).Error + if err != nil { + t.Fatal(err) + } + + // Delete the login source will result in error + err = db.DeleteByID(source.ID) + expErr := ErrLoginSourceInUse{args: errutil.Args{"id": source.ID}} + assert.Equal(t, expErr, err) + }) + + setMockLoginSourceFilesStore(t, db, &mockLoginSourceFilesStore{ + MockGetByID: func(id int64) (*LoginSource, error) { + return nil, ErrLoginSourceNotExist{args: errutil.Args{"id": id}} + }, + }) + + // Create a login source with name "GitHub2" + source, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub2", + Activated: true, + Default: false, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + // Delete a non-existent ID is noop + err = db.DeleteByID(9999) + if err != nil { + t.Fatal(err) + } + + // We should be able to get it back + _, err = db.GetByID(source.ID) + if err != nil { + t.Fatal(err) + } + + // Now delete this login source with ID + err = db.DeleteByID(source.ID) + if err != nil { + t.Fatal(err) + } + + // We should get token not found error + _, err = db.GetByID(source.ID) + expErr := ErrLoginSourceNotExist{args: errutil.Args{"id": source.ID}} + assert.Equal(t, expErr, err) +} + +func test_loginSources_GetByID(t *testing.T, db *loginSources) { + setMockLoginSourceFilesStore(t, db, &mockLoginSourceFilesStore{ + MockGetByID: func(id int64) (*LoginSource, error) { + if id != 101 { + return nil, ErrLoginSourceNotExist{args: errutil.Args{"id": id}} + } + return &LoginSource{ID: id}, nil + }, + }) + + expConfig := &GitHubConfig{ + APIEndpoint: "https://api.github.com", + } + + // Create a login source with name "GitHub" + source, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Default: false, + Config: expConfig, + }) + if err != nil { + t.Fatal(err) + } + + // Get the one in the database and test the read/write hooks + source, err = db.GetByID(source.ID) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, expConfig, source.Config) + + // Get the one in source file store + _, err = db.GetByID(101) + if err != nil { + t.Fatal(err) + } +} + +func test_loginSources_List(t *testing.T, db *loginSources) { + setMockLoginSourceFilesStore(t, db, &mockLoginSourceFilesStore{ + MockList: func(opts ListLoginSourceOpts) []*LoginSource { + if opts.OnlyActivated { + return []*LoginSource{ + {ID: 1}, + } + } + return []*LoginSource{ + {ID: 1}, + {ID: 2}, + } + }, + }) + + // Create two login sources in database, one activated and the other one not + _, err := db.Create(CreateLoginSourceOpts{ + Type: LoginPAM, + Name: "PAM", + Config: &PAMConfig{ + ServiceName: "PAM", + }, + }) + if err != nil { + t.Fatal(err) + } + _, err = db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + // List all login sources + sources, err := db.List(ListLoginSourceOpts{}) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 4, len(sources), "number of sources") + + // Only list activated login sources + sources, err = db.List(ListLoginSourceOpts{OnlyActivated: true}) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(sources), "number of sources") +} + +func test_loginSources_ResetNonDefault(t *testing.T, db *loginSources) { + setMockLoginSourceFilesStore(t, db, &mockLoginSourceFilesStore{ + MockList: func(opts ListLoginSourceOpts) []*LoginSource { + return []*LoginSource{ + { + File: &mockLoginSourceFileStore{ + MockSetGeneral: func(name, value string) { + assert.Equal(t, "is_default", name) + assert.Equal(t, "false", value) + }, + MockSave: func() error { + return nil + }, + }, + }, + } + }, + MockUpdate: func(source *LoginSource) {}, + }) + + // Create two login sources both have default on + source1, err := db.Create(CreateLoginSourceOpts{ + Type: LoginPAM, + Name: "PAM", + Default: true, + Config: &PAMConfig{ + ServiceName: "PAM", + }, + }) + if err != nil { + t.Fatal(err) + } + source2, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Default: true, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + // Set source 1 as default + err = db.ResetNonDefault(source1) + if err != nil { + t.Fatal(err) + } + + // Verify the default state + source1, err = db.GetByID(source1.ID) + if err != nil { + t.Fatal(err) + } + assert.True(t, source1.IsDefault) + + source2, err = db.GetByID(source2.ID) + if err != nil { + t.Fatal(err) + } + assert.False(t, source2.IsDefault) +} + +func test_loginSources_Save(t *testing.T, db *loginSources) { + t.Run("save to database", func(t *testing.T) { + // Create a login source with name "GitHub" + source, err := db.Create(CreateLoginSourceOpts{ + Type: LoginGitHub, + Name: "GitHub", + Activated: true, + Default: false, + Config: &GitHubConfig{ + APIEndpoint: "https://api.github.com", + }, + }) + if err != nil { + t.Fatal(err) + } + + source.IsActived = false + source.Config = &GitHubConfig{ + APIEndpoint: "https://api2.github.com", + } + err = db.Save(source) + if err != nil { + t.Fatal(err) + } + + source, err = db.GetByID(source.ID) + if err != nil { + t.Fatal(err) + } + assert.False(t, source.IsActived) + assert.Equal(t, "https://api2.github.com", source.GitHub().APIEndpoint) + }) + + t.Run("save to file", func(t *testing.T) { + calledSave := false + source := &LoginSource{ + File: &mockLoginSourceFileStore{ + MockSetGeneral: func(name, value string) {}, + MockSetConfig: func(cfg interface{}) error { return nil }, + MockSave: func() error { + calledSave = true + return nil + }, + }, + } + err := db.Save(source) + if err != nil { + t.Fatal(err) + } + assert.True(t, calledSave) + }) +} diff --git a/internal/db/main_test.go b/internal/db/main_test.go index fdd7447fb..172f00feb 100644 --- a/internal/db/main_test.go +++ b/internal/db/main_test.go @@ -41,7 +41,8 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func deleteTables(db *gorm.DB, tables ...interface{}) error { +// clearTables removes all rows from given tables. +func clearTables(db *gorm.DB, tables ...interface{}) error { for _, t := range tables { err := db.Delete(t).Error if err != nil { diff --git a/internal/db/mocks.go b/internal/db/mocks.go index 7e6ddced9..9b1e90286 100644 --- a/internal/db/mocks.go +++ b/internal/db/mocks.go @@ -78,6 +78,59 @@ func SetMockLFSStore(t *testing.T, mock LFSStore) { }) } +var _ loginSourceFilesStore = (*mockLoginSourceFilesStore)(nil) + +type mockLoginSourceFilesStore struct { + MockGetByID func(id int64) (*LoginSource, error) + MockLen func() int + MockList func(opts ListLoginSourceOpts) []*LoginSource + MockUpdate func(source *LoginSource) +} + +func (m *mockLoginSourceFilesStore) GetByID(id int64) (*LoginSource, error) { + return m.MockGetByID(id) +} + +func (m *mockLoginSourceFilesStore) Len() int { + return m.MockLen() +} + +func (m *mockLoginSourceFilesStore) List(opts ListLoginSourceOpts) []*LoginSource { + return m.MockList(opts) +} + +func (m *mockLoginSourceFilesStore) Update(source *LoginSource) { + m.MockUpdate(source) +} + +func setMockLoginSourceFilesStore(t *testing.T, db *loginSources, mock loginSourceFilesStore) { + before := db.files + db.files = mock + t.Cleanup(func() { + db.files = before + }) +} + +var _ loginSourceFileStore = (*mockLoginSourceFileStore)(nil) + +type mockLoginSourceFileStore struct { + MockSetGeneral func(name, value string) + MockSetConfig func(cfg interface{}) error + MockSave func() error +} + +func (m *mockLoginSourceFileStore) SetGeneral(name, value string) { + m.MockSetGeneral(name, value) +} + +func (m *mockLoginSourceFileStore) SetConfig(cfg interface{}) error { + return m.MockSetConfig(cfg) +} + +func (m *mockLoginSourceFileStore) Save() error { + return m.MockSave() +} + var _ PermsStore = (*MockPermsStore)(nil) type MockPermsStore struct { diff --git a/internal/db/models.go b/internal/db/models.go index 9b5b0d9a1..37783a13e 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -53,7 +53,7 @@ func init() { new(Watch), new(Star), new(Follow), new(Action), new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser), new(Label), new(IssueLabel), new(Milestone), - new(Mirror), new(Release), new(LoginSource), new(Webhook), new(HookTask), + new(Mirror), new(Release), new(Webhook), new(HookTask), new(ProtectBranch), new(ProtectBranchWhitelist), new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), new(Notice), new(EmailAddress)) @@ -200,7 +200,7 @@ func GetStatistic() (stats Statistic) { stats.Counter.Follow, _ = x.Count(new(Follow)) stats.Counter.Mirror, _ = x.Count(new(Mirror)) stats.Counter.Release, _ = x.Count(new(Release)) - stats.Counter.LoginSource = CountLoginSources() + stats.Counter.LoginSource = LoginSources.Count() stats.Counter.Webhook, _ = x.Count(new(Webhook)) stats.Counter.Milestone, _ = x.Count(new(Milestone)) stats.Counter.Label, _ = x.Count(new(Label)) @@ -295,13 +295,13 @@ func ImportDatabase(dirPath string, verbose bool) (err error) { tp := LoginType(com.StrTo(com.ToStr(meta["Type"])).MustInt64()) switch tp { case LoginLDAP, LoginDLDAP: - bean.Cfg = new(LDAPConfig) + bean.Config = new(LDAPConfig) case LoginSMTP: - bean.Cfg = new(SMTPConfig) + bean.Config = new(SMTPConfig) case LoginPAM: - bean.Cfg = new(PAMConfig) + bean.Config = new(PAMConfig) case LoginGitHub: - bean.Cfg = new(GitHubConfig) + bean.Config = new(GitHubConfig) default: return fmt.Errorf("unrecognized login source type:: %v", tp) } diff --git a/internal/db/perms_test.go b/internal/db/perms_test.go index 9f3f4b1b7..ea4ed1ea9 100644 --- a/internal/db/perms_test.go +++ b/internal/db/perms_test.go @@ -17,8 +17,9 @@ func Test_perms(t *testing.T) { t.Parallel() + tables := []interface{}{new(Access)} db := &perms{ - DB: initTestDB(t, "perms", new(Access)), + DB: initTestDB(t, "perms", tables...), } for _, tc := range []struct { @@ -31,7 +32,7 @@ func Test_perms(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { - err := deleteTables(db.DB, new(Access)) + err := clearTables(db.DB, tables...) if err != nil { t.Fatal(err) } diff --git a/internal/db/user.go b/internal/db/user.go index a61bb6875..11697bc77 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -48,33 +48,33 @@ const ( // User represents the object of individual and member of organization. type User struct { ID int64 - LowerName string `xorm:"UNIQUE NOT NULL"` - Name string `xorm:"UNIQUE NOT NULL"` + LowerName string `xorm:"UNIQUE NOT NULL" gorm:"UNIQUE"` + Name string `xorm:"UNIQUE NOT NULL" gorm:"NOT NULL"` FullName string // Email is the primary email address (to be used for communication) - Email string `xorm:"NOT NULL"` - Passwd string `xorm:"NOT NULL"` + Email string `xorm:"NOT NULL" gorm:"NOT NULL"` + Passwd string `xorm:"NOT NULL" gorm:"NOT NULL"` LoginType LoginType - LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` + LoginSource int64 `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"` LoginName string Type UserType - OwnedOrgs []*User `xorm:"-" json:"-"` - Orgs []*User `xorm:"-" json:"-"` - Repos []*Repository `xorm:"-" json:"-"` + OwnedOrgs []*User `xorm:"-" gorm:"-" json:"-"` + Orgs []*User `xorm:"-" gorm:"-" json:"-"` + Repos []*Repository `xorm:"-" gorm:"-" json:"-"` Location string Website string - Rands string `xorm:"VARCHAR(10)"` - Salt string `xorm:"VARCHAR(10)"` + Rands string `xorm:"VARCHAR(10)" gorm:"TYPE:VARCHAR(10)"` + Salt string `xorm:"VARCHAR(10)" gorm:"TYPE:VARCHAR(10)"` - Created time.Time `xorm:"-" json:"-"` + Created time.Time `xorm:"-" gorm:"-" json:"-"` CreatedUnix int64 - Updated time.Time `xorm:"-" json:"-"` + Updated time.Time `xorm:"-" gorm:"-" json:"-"` UpdatedUnix int64 // Remember visibility choice for convenience, true for private LastRepoVisibility bool - // Maximum repository creation limit, -1 means use gloabl default - MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"` + // Maximum repository creation limit, -1 means use global default + MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1" gorm:"NOT NULL;DEFAULT:-1"` // Permissions IsActive bool // Activate primary email @@ -84,13 +84,13 @@ type User struct { ProhibitLogin bool // Avatar - Avatar string `xorm:"VARCHAR(2048) NOT NULL"` - AvatarEmail string `xorm:"NOT NULL"` + Avatar string `xorm:"VARCHAR(2048) NOT NULL" gorm:"TYPE:VARCHAR(2048);NOT NULL"` + AvatarEmail string `xorm:"NOT NULL" gorm:"NOT NULL"` UseCustomAvatar bool // Counters NumFollowers int - NumFollowing int `xorm:"NOT NULL DEFAULT 0"` + NumFollowing int `xorm:"NOT NULL DEFAULT 0" gorm:"NOT NULL;DEFAULT:0"` NumStars int NumRepos int @@ -98,8 +98,8 @@ type User struct { Description string NumTeams int NumMembers int - Teams []*Team `xorm:"-" json:"-"` - Members []*User `xorm:"-" json:"-"` + Teams []*Team `xorm:"-" gorm:"-" json:"-"` + Members []*User `xorm:"-" gorm:"-" json:"-"` } func (u *User) BeforeInsert() { diff --git a/internal/dbutil/writer.go b/internal/dbutil/writer.go index 6b83d5375..3b8d7363a 100644 --- a/internal/dbutil/writer.go +++ b/internal/dbutil/writer.go @@ -29,6 +29,8 @@ func (w *Writer) Print(v ...interface{}) { fmt.Fprintf(w.Writer, "[sql] [%s] [%s] %s %v (%d rows affected)", v[1:]...) case "log": fmt.Fprintf(w.Writer, "[log] [%s] %s", v[1:]...) + case "error": + fmt.Fprintf(w.Writer, "[err] [%s] %s", v[1:]...) default: fmt.Fprint(w.Writer, v...) } diff --git a/internal/dbutil/writer_test.go b/internal/dbutil/writer_test.go index 33961aa1e..a484d4425 100644 --- a/internal/dbutil/writer_test.go +++ b/internal/dbutil/writer_test.go @@ -41,6 +41,11 @@ func TestWriter_Print(t *testing.T) { vs: []interface{}{"log", "writer.go:65", "something"}, expOutput: "[log] [writer.go:65] something", }, + { + name: "error", + vs: []interface{}{"error", "writer.go:65", "something bad"}, + expOutput: "[err] [writer.go:65] something bad", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/internal/osutil/osutil.go b/internal/osutil/osutil.go index 3af67570d..b2205a46b 100644 --- a/internal/osutil/osutil.go +++ b/internal/osutil/osutil.go @@ -17,6 +17,16 @@ func IsFile(path string) bool { return !f.IsDir() } +// IsDir returns true if given path is a directory, and returns false when it's +// a file or does not exist. +func IsDir(dir string) bool { + f, e := os.Stat(dir) + if e != nil { + return false + } + return f.IsDir() +} + // IsExist returns true if a file or directory exists. func IsExist(path string) bool { _, err := os.Stat(path) diff --git a/internal/osutil/osutil_test.go b/internal/osutil/osutil_test.go index 8c45f5c0a..ca2c75bf3 100644 --- a/internal/osutil/osutil_test.go +++ b/internal/osutil/osutil_test.go @@ -33,6 +33,29 @@ func TestIsFile(t *testing.T) { } } +func TestIsDir(t *testing.T) { + tests := []struct { + path string + expVal bool + }{ + { + path: "osutil.go", + expVal: false, + }, { + path: "../osutil", + expVal: true, + }, { + path: "not_found", + expVal: false, + }, + } + for _, test := range tests { + t.Run("", func(t *testing.T) { + assert.Equal(t, test.expVal, IsDir(test.path)) + }) + } +} + func TestIsExist(t *testing.T) { tests := []struct { path string diff --git a/internal/route/admin/auths.go b/internal/route/admin/auths.go index c5c1e480f..d2967e293 100644 --- a/internal/route/admin/auths.go +++ b/internal/route/admin/auths.go @@ -11,7 +11,6 @@ import ( "github.com/unknwon/com" log "unknwon.dev/clog/v2" - "xorm.io/core" "gogs.io/gogs/internal/auth/ldap" "gogs.io/gogs/internal/conf" @@ -32,13 +31,13 @@ func Authentications(c *context.Context) { c.PageIs("AdminAuthentications") var err error - c.Data["Sources"], err = db.ListLoginSources() + c.Data["Sources"], err = db.LoginSources.List(db.ListLoginSourceOpts{}) if err != nil { c.Error(err, "list login sources") return } - c.Data["Total"] = db.CountLoginSources() + c.Data["Total"] = db.LoginSources.Count() c.Success(AUTHS) } @@ -56,9 +55,9 @@ var ( {db.LoginNames[db.LoginGitHub], db.LoginGitHub}, } securityProtocols = []dropdownItem{ - {db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED], ldap.SECURITY_PROTOCOL_UNENCRYPTED}, - {db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_LDAPS], ldap.SECURITY_PROTOCOL_LDAPS}, - {db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_START_TLS], ldap.SECURITY_PROTOCOL_START_TLS}, + {db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted}, + {db.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS}, + {db.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS}, } ) @@ -69,7 +68,7 @@ func NewAuthSource(c *context.Context) { c.Data["type"] = db.LoginLDAP c.Data["CurrentTypeName"] = db.LoginNames[db.LoginLDAP] - c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED] + c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted] c.Data["smtp_auth"] = "PLAIN" c.Data["is_active"] = true c.Data["is_default"] = true @@ -81,7 +80,7 @@ func NewAuthSource(c *context.Context) { func parseLDAPConfig(f form.Authentication) *db.LDAPConfig { return &db.LDAPConfig{ - Source: &ldap.Source{ + Source: ldap.Source{ Host: f.Host, Port: f.Port, SecurityProtocol: ldap.SecurityProtocol(f.SecurityProtocol), @@ -129,11 +128,11 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) { c.Data["SMTPAuths"] = db.SMTPAuths hasTLS := false - var config core.Conversion + var config interface{} switch db.LoginType(f.Type) { case db.LoginLDAP, db.LoginDLDAP: config = parseLDAPConfig(f) - hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SECURITY_PROTOCOL_UNENCRYPTED + hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SecurityProtocolUnencrypted case db.LoginSMTP: config = parseSMTPConfig(f) hasTLS = true @@ -156,22 +155,31 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) { return } - if err := db.CreateLoginSource(&db.LoginSource{ + source, err := db.LoginSources.Create(db.CreateLoginSourceOpts{ Type: db.LoginType(f.Type), Name: f.Name, - IsActived: f.IsActive, - IsDefault: f.IsDefault, - Cfg: config, - }); err != nil { + Activated: f.IsActive, + Default: f.IsDefault, + Config: config, + }) + if err != nil { if db.IsErrLoginSourceAlreadyExist(err) { c.FormErr("Name") - c.RenderWithErr(c.Tr("admin.auths.login_source_exist", err.(db.ErrLoginSourceAlreadyExist).Name), AUTH_NEW, f) + c.RenderWithErr(c.Tr("admin.auths.login_source_exist", f.Name), AUTH_NEW, f) } else { c.Error(err, "create login source") } return } + if source.IsDefault { + err = db.LoginSources.ResetNonDefault(source) + if err != nil { + c.Error(err, "reset non-default login sources") + return + } + } + log.Trace("Authentication created by admin(%s): %s", c.User.Name, f.Name) c.Flash.Success(c.Tr("admin.auths.new_success", f.Name)) @@ -217,7 +225,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) { return } - var config core.Conversion + var config interface{} switch db.LoginType(f.Type) { case db.LoginLDAP, db.LoginDLDAP: config = parseLDAPConfig(f) @@ -239,12 +247,20 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) { source.Name = f.Name source.IsActived = f.IsActive source.IsDefault = f.IsDefault - source.Cfg = config - if err := db.UpdateLoginSource(source); err != nil { + source.Config = config + if err := db.LoginSources.Save(source); err != nil { c.Error(err, "update login source") return } + if source.IsDefault { + err = db.LoginSources.ResetNonDefault(source) + if err != nil { + c.Error(err, "reset non-default login sources") + return + } + } + log.Trace("Authentication changed by admin '%s': %d", c.User.Name, source.ID) c.Flash.Success(c.Tr("admin.auths.update_success")) @@ -252,13 +268,8 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) { } func DeleteAuthSource(c *context.Context) { - source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid")) - if err != nil { - c.Error(err, "get login source by ID") - return - } - - if err = db.DeleteSource(source); err != nil { + id := c.ParamsInt64(":authid") + if err := db.LoginSources.DeleteByID(id); err != nil { if db.IsErrLoginSourceInUse(err) { c.Flash.Error(c.Tr("admin.auths.still_in_used")) } else { @@ -269,7 +280,7 @@ func DeleteAuthSource(c *context.Context) { }) return } - log.Trace("Authentication deleted by admin(%s): %d", c.User.Name, source.ID) + log.Trace("Authentication deleted by admin(%s): %d", c.User.Name, id) c.Flash.Success(c.Tr("admin.auths.deletion_success")) c.JSONSuccess(map[string]interface{}{ diff --git a/internal/route/admin/users.go b/internal/route/admin/users.go index 96257c59a..caf2109ee 100644 --- a/internal/route/admin/users.go +++ b/internal/route/admin/users.go @@ -46,7 +46,7 @@ func NewUser(c *context.Context) { c.Data["login_type"] = "0-0" - sources, err := db.ListLoginSources() + sources, err := db.LoginSources.List(db.ListLoginSourceOpts{}) if err != nil { c.Error(err, "list login sources") return @@ -62,7 +62,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) { c.Data["PageIsAdmin"] = true c.Data["PageIsAdminUsers"] = true - sources, err := db.ListLoginSources() + sources, err := db.LoginSources.List(db.ListLoginSourceOpts{}) if err != nil { c.Error(err, "list login sources") return @@ -141,7 +141,7 @@ func prepareUserInfo(c *context.Context) *db.User { c.Data["LoginSource"] = &db.LoginSource{} } - sources, err := db.ListLoginSources() + sources, err := db.LoginSources.List(db.ListLoginSourceOpts{}) if err != nil { c.Error(err, "list login sources") return nil diff --git a/internal/route/api/v1/admin/user.go b/internal/route/api/v1/admin/user.go index 06c6569f4..0593475ad 100644 --- a/internal/route/api/v1/admin/user.go +++ b/internal/route/api/v1/admin/user.go @@ -13,7 +13,6 @@ import ( "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/context" "gogs.io/gogs/internal/db" - "gogs.io/gogs/internal/db/errors" "gogs.io/gogs/internal/email" "gogs.io/gogs/internal/route/api/v1/user" ) @@ -25,7 +24,7 @@ func parseLoginSource(c *context.APIContext, u *db.User, sourceID int64, loginNa source, err := db.LoginSources.GetByID(sourceID) if err != nil { - if errors.IsLoginSourceNotExist(err) { + if db.IsErrLoginSourceNotExist(err) { c.ErrorStatus(http.StatusUnprocessableEntity, err) } else { c.Error(err, "get login source by ID") diff --git a/internal/route/install.go b/internal/route/install.go index fbb605f0c..5bbf943ad 100644 --- a/internal/route/install.go +++ b/internal/route/install.go @@ -76,7 +76,6 @@ func GlobalInit(customConf string) error { } db.HasEngine = true - db.LoadAuthSources() db.LoadRepoConfig() db.NewRepoContext() diff --git a/internal/route/user/auth.go b/internal/route/user/auth.go index 22e8176c3..be23b78d0 100644 --- a/internal/route/user/auth.go +++ b/internal/route/user/auth.go @@ -101,7 +101,7 @@ func Login(c *context.Context) { } // Display normal login page - loginSources, err := db.ActivatedLoginSources() + loginSources, err := db.LoginSources.List(db.ListLoginSourceOpts{OnlyActivated: true}) if err != nil { c.Error(err, "list activated login sources") return @@ -148,7 +148,7 @@ func afterLogin(c *context.Context, u *db.User, remember bool) { func LoginPost(c *context.Context, f form.SignIn) { c.Title("sign_in") - loginSources, err := db.ActivatedLoginSources() + loginSources, err := db.LoginSources.List(db.ListLoginSourceOpts{OnlyActivated: true}) if err != nil { c.Error(err, "list activated login sources") return diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index c09615dbf..d6b9cc682 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -176,7 +176,7 @@ {{end}} - +