aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/assets/templates/templates_gen.go12
-rw-r--r--internal/auth/ldap/ldap.go10
-rw-r--r--internal/db/access_tokens_test.go13
-rw-r--r--internal/db/db.go9
-rw-r--r--internal/db/error.go33
-rw-r--r--internal/db/errors/login_source.go14
-rw-r--r--internal/db/lfs_test.go5
-rw-r--r--internal/db/login_source.go564
-rw-r--r--internal/db/login_source_files.go212
-rw-r--r--internal/db/login_sources.go276
-rw-r--r--internal/db/login_sources_test.go389
-rw-r--r--internal/db/main_test.go3
-rw-r--r--internal/db/mocks.go53
-rw-r--r--internal/db/models.go12
-rw-r--r--internal/db/perms_test.go5
-rw-r--r--internal/db/user.go38
-rw-r--r--internal/dbutil/writer.go2
-rw-r--r--internal/dbutil/writer_test.go5
-rw-r--r--internal/osutil/osutil.go10
-rw-r--r--internal/osutil/osutil_test.go23
-rw-r--r--internal/route/admin/auths.go65
-rw-r--r--internal/route/admin/users.go6
-rw-r--r--internal/route/api/v1/admin/user.go3
-rw-r--r--internal/route/install.go1
-rw-r--r--internal/route/user/auth.go4
-rw-r--r--templates/admin/auth/edit.tmpl4
26 files changed, 1119 insertions, 652 deletions
diff --git a/internal/assets/templates/templates_gen.go b/internal/assets/templates/templates_gen.go
index b5719801..6d786759 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 29d95560..d8f8458e 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 d3dfb83f..0f979e95 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 1be2cc4b..77d78f53 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 ed173d86..46e7dde5 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 876a0820..db0cd1f9 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 6eb14019..29c7d665 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 b6665f4e..e821e186 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 00000000..4c5cb377
--- /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 41ee73b9..b620a442 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 00000000..8cc0ab19
--- /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 fdd7447f..172f00fe 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 7e6ddced..9b1e9028 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 9b5b0d9a..37783a13 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 9f3f4b1b..ea4ed1ea 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 a61bb687..11697bc7 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 6b83d537..3b8d7363 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 33961aa1..a484d442 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 3af67570..b2205a46 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 8c45f5c0..ca2c75bf 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 c5c1e480..d2967e29 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 96257c59..caf2109e 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 06c6569f..0593475a 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 fbb605f0..5bbf943a 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 22e8176c..be23b78d 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 c09615db..d6b9cc68 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -176,7 +176,7 @@
<input id="github_api_endpoint" name="github_api_endpoint" value="{{$cfg.APIEndpoint}}" placeholder="e.g. https://api.github.com/" required>
</div>
{{end}}
-
+
<div class="inline field {{if not .Source.IsSMTP}}hide{{end}}">
<div class="ui checkbox">
<label><strong>{{.i18n.Tr "admin.auths.enable_tls"}}</strong></label>
@@ -203,7 +203,7 @@
</div>
<div class="field">
<button class="ui green button">{{.i18n.Tr "admin.auths.update"}}</button>
- {{if not .Source.LocalFile}}
+ {{if not .Source.File}}
<div class="ui red button delete-button" data-url="{{$.Link}}/delete" data-id="{{.Source.ID}}">{{.i18n.Tr "admin.auths.delete"}}</div>
{{end}}
</div>