Adding authorisation options for API access

- ldap currently the only supported method
adding authorisation options for local repositories
  - ldap groups per repo
This commit is contained in:
bpiraeus
2022-03-13 16:07:21 -07:00
committed by André Roth
parent ab18da351d
commit 836137f15d
14 changed files with 358 additions and 66 deletions
+119
View File
@@ -0,0 +1,119 @@
package api
import (
"crypto/tls"
"fmt"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3"
)
func Authorize(username string, password string) (ok bool) {
config := context.Config()
if config.Auth.Type != "" {
switch strings.ToLower(config.Auth.Type) {
case "ldap":
ok = doLdapAuth(username, password)
default:
return false
}
if ok != true {
return false
}
}
return true
}
func doLdapAuth(username string, password string) bool {
config := context.Config()
attributes := []string{"DN", "CN"}
server := config.Auth.Server
dn := config.Auth.LdapDN
filter := fmt.Sprintf(config.Auth.LdapFilter, username)
// connect to ldap server
conn, err := ldap.Dial("tcp", server)
if err != nil {
return false
}
defer conn.Close()
// reconnect via tls
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: config.Auth.SecureTLS})
if err != nil {
return false
}
// format our request and then fire it off
request := ldap.NewSearchRequest(dn, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, attributes, nil)
search, err := conn.Search(request)
if err != nil {
return false
}
// get our modified dn and then check our user for auth
udn := search.Entries[0].DN
err = conn.Bind(udn, password)
if err != nil {
return false
}
return true
}
func getGroups(c *gin.Context, username string) {
var groups []string
config := context.Config()
dn := fmt.Sprintf("%s", config.Auth.LdapDN)
session := sessions.Default(c)
// connect to ldap server
server := fmt.Sprintf("%s", config.Auth.Server)
conn, err := ldap.Dial("tcp", server)
if err != nil {
return
}
// reconnect via tls
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return
}
filter := fmt.Sprintf("(|(member=uid=%s,ou=people,dc=llnw,dc=com)(member=uid=%s,ou=people,dc=llnw,dc=com))", username, username)
request := ldap.NewSearchRequest(dn, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"dn", "cn"}, nil)
search, err := conn.Search(request)
if err != nil {
return
}
if len(search.Entries) < 1 {
return
}
for _, v := range search.Entries {
value := strings.Split(strings.TrimLeft(v.DN, "cn="), ",")[0]
groups = append(groups, fmt.Sprintf("%s,", value))
}
session.Set("Groups", groups)
return
}
func checkGroup(c *gin.Context, ldgroup string) bool {
session := sessions.Default(c)
groups := session.Get("Groups")
if ldgroup == "" {
return true
}
for _, v := range groups.([]string) {
if strings.Contains(v, ldgroup) {
return true
}
}
return false
}
func CheckGroup(c *gin.Context, ldgroup string) (err error) {
if !checkGroup(c, ldgroup) {
err = fmt.Errorf("Authorisation Failred")
}
return err
}
+6
View File
@@ -267,7 +267,13 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
return
}
err = CheckGroup(c, localRepo.LdapGroup)
if err != nil {
c.AbortWithError(403, err)
}
resources = append(resources, string(localRepo.Key()))
sources = append(sources, localRepo)
}
} else {
+35
View File
@@ -95,6 +95,8 @@ type repoCreateParams struct {
DefaultComponent string ` json:"DefaultComponent" example:"main"`
// Snapshot name to create repoitory from (optional)
FromSnapshot string ` json:"FromSnapshot" example:""`
//
LdapGroup string
}
// @Summary Create Repository
@@ -125,6 +127,7 @@ func apiReposCreate(c *gin.Context) {
repo := deb.NewLocalRepo(b.Name, b.Comment)
repo.DefaultComponent = b.DefaultComponent
repo.DefaultDistribution = b.DefaultDistribution
repo.LdapGroup = b.LdapGroup
collectionFactory := context.NewCollectionFactory()
@@ -173,6 +176,8 @@ type reposEditParams struct {
DefaultDistribution *string ` json:"DefaultDistribution" example:""`
// Change Devault Component for publishing
DefaultComponent *string ` json:"DefaultComponent" example:""`
//
LdapGroup *string
}
// @Summary Update Repository
@@ -199,6 +204,12 @@ func apiReposEdit(c *gin.Context) {
return
}
err = CheckGroup(c, repo.LdapGroup)
if err != nil {
c.AbortWithError(403, err)
return
}
if b.Name != nil {
_, err := collection.ByName(*b.Name)
if err == nil {
@@ -217,6 +228,9 @@ func apiReposEdit(c *gin.Context) {
if b.DefaultComponent != nil {
repo.DefaultComponent = *b.DefaultComponent
}
if b.LdapGroup != nil {
repo.LdapGroup = *b.LdapGroup
}
err = collection.Update(repo)
if err != nil {
@@ -276,6 +290,12 @@ func apiReposDrop(c *gin.Context) {
return
}
err = CheckGroup(c, repo.LdapGroup)
if err != nil {
c.AbortWithError(403, err)
return
}
resources := []string{string(repo.Key())}
taskName := fmt.Sprintf("Delete repo %s", name)
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
@@ -365,6 +385,11 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
}
err = CheckGroup(c, repo.LdapGroup)
if err != nil {
return &task.ProcessReturnValue{Code: 403, Value: nil}, err
}
out.Printf("Loading packages...\n")
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), nil)
if err != nil {
@@ -522,6 +547,11 @@ func apiReposPackageFromDir(c *gin.Context) {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
}
err = CheckGroup(c, repo.LdapGroup)
if err != nil {
return &task.ProcessReturnValue{Code: 403, Value: nil}, err
}
verifier := context.GetVerifier()
var (
@@ -845,6 +875,11 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
AbortWithJSONError(c, 404, err)
return
}
err = CheckGroup(c, repo.LdapGroup)
if err != nil {
c.AbortWithError(403, err)
return
}
resources = append(resources, string(repo.Key()))
}
+132 -63
View File
@@ -1,9 +1,12 @@
package api
import (
"fmt"
"log"
"net/http"
"os"
"sync/atomic"
"time"
"github.com/aptly-dev/aptly/aptly"
ctx "github.com/aptly-dev/aptly/context"
@@ -15,6 +18,10 @@ import (
"github.com/aptly-dev/aptly/docs"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
uuid "github.com/nu7hatch/gouuid"
)
var context *ctx.AptlyContext
@@ -133,105 +140,167 @@ func Router(c *ctx.AptlyContext) http.Handler {
api.GET("/healthy", apiHealthy)
}
// set up cookies and sessions
token, err := uuid.NewV4()
if err != nil {
panic(err)
}
store := cookie.NewStore([]byte(token.String()))
router.Use(sessions.Sessions(token.String(), store))
// prep our config fetcher ahead of need
config := context.Config()
// prep a logfile if we've set one
if config.LogFile != "" {
file, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
defer file.Close()
log.SetOutput(file)
}
router.GET("/version", apiVersion)
var username string
var password string
router.POST("/login", func(c *gin.Context) {
session := sessions.Default(c)
session.Options(sessions.Options{MaxAge: 30})
if config.UseAuth {
log.Printf("UseAuth is enabled\n")
username = c.PostForm("username")
password = c.PostForm("password")
if !Authorize(username, password) {
c.AbortWithError(403, fmt.Errorf("Authorization Failure"))
}
log.Printf("%s authorized from %s\n", username, c.ClientIP())
}
session.Set(token.String(), time.Now().Unix())
session.Save()
getGroups(c, username)
c.String(200, "Authorized!")
})
router.POST("/logout", func(c *gin.Context) {
session := sessions.Default(c)
session.Options(sessions.Options{MaxAge: -1})
session.Save()
c.String(200, "Deauthorized")
})
authorize := router.Group("/api", func(c *gin.Context) {
session := sessions.Default(c)
if config.UseAuth {
if session.Get(token.String()) == nil {
c.AbortWithError(403, fmt.Errorf("not authorized"))
}
session.Options(sessions.Options{MaxAge: 30})
session.Set(token.String(), time.Now().Unix())
session.Save()
}
})
{
api.GET("/repos", apiReposList)
api.POST("/repos", apiReposCreate)
api.GET("/repos/:name", apiReposShow)
api.PUT("/repos/:name", apiReposEdit)
api.DELETE("/repos/:name", apiReposDrop)
authorize.GET("/repos", apiReposList)
authorize.POST("/repos", apiReposCreate)
authorize.GET("/repos/:name", apiReposShow)
authorize.PUT("/repos/:name", apiReposEdit)
authorize.DELETE("/repos/:name", apiReposDrop)
api.GET("/repos/:name/packages", apiReposPackagesShow)
api.POST("/repos/:name/packages", apiReposPackagesAdd)
api.DELETE("/repos/:name/packages", apiReposPackagesDelete)
authorize.GET("/repos/:name/packages", apiReposPackagesShow)
authorize.POST("/repos/:name/packages", apiReposPackagesAdd)
authorize.DELETE("/repos/:name/packages", apiReposPackagesDelete)
api.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
api.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
api.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage)
authorize.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
authorize.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
authorize.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage)
api.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile)
api.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir)
authorize.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile)
authorize.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir)
api.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
authorize.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
}
{
api.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
authorize.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
}
{
api.GET("/mirrors", apiMirrorsList)
api.GET("/mirrors/:name", apiMirrorsShow)
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
api.POST("/mirrors", apiMirrorsCreate)
api.PUT("/mirrors/:name", apiMirrorsUpdate)
api.DELETE("/mirrors/:name", apiMirrorsDrop)
authorize.GET("/mirrors", apiMirrorsList)
authorize.GET("/mirrors/:name", apiMirrorsShow)
authorize.GET("/mirrors/:name/packages", apiMirrorsPackages)
authorize.POST("/mirrors", apiMirrorsCreate)
authorize.PUT("/mirrors/:name", apiMirrorsUpdate)
authorize.DELETE("/mirrors/:name", apiMirrorsDrop)
}
{
api.POST("/gpg/key", apiGPGAddKey)
authorize.POST("/gpg/key", apiGPGAddKey)
}
{
api.GET("/s3", apiS3List)
authorize.GET("/s3", apiS3List)
}
{
api.GET("/files", apiFilesListDirs)
api.POST("/files/:dir", apiFilesUpload)
api.GET("/files/:dir", apiFilesListFiles)
api.DELETE("/files/:dir", apiFilesDeleteDir)
api.DELETE("/files/:dir/:name", apiFilesDeleteFile)
authorize.GET("/files", apiFilesListDirs)
authorize.POST("/files/:dir", apiFilesUpload)
authorize.GET("/files/:dir", apiFilesListFiles)
authorize.DELETE("/files/:dir", apiFilesDeleteDir)
authorize.DELETE("/files/:dir/:name", apiFilesDeleteFile)
}
{
api.GET("/publish", apiPublishList)
api.GET("/publish/:prefix/:distribution", apiPublishShow)
api.POST("/publish", apiPublishRepoOrSnapshot)
api.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
api.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
api.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
api.POST("/publish/:prefix/:distribution/sources", apiPublishAddSource)
api.GET("/publish/:prefix/:distribution/sources", apiPublishListChanges)
api.PUT("/publish/:prefix/:distribution/sources", apiPublishSetSources)
api.DELETE("/publish/:prefix/:distribution/sources", apiPublishDropChanges)
api.PUT("/publish/:prefix/:distribution/sources/:component", apiPublishUpdateSource)
api.DELETE("/publish/:prefix/:distribution/sources/:component", apiPublishRemoveSource)
api.POST("/publish/:prefix/:distribution/update", apiPublishUpdate)
authorize.GET("/publish", apiPublishList)
authorize.GET("/publish/:prefix/:distribution", apiPublishShow)
authorize.POST("/publish", apiPublishRepoOrSnapshot)
authorize.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
authorize.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
authorize.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
authorize.POST("/publish/:prefix/:distribution/sources", apiPublishAddSource)
authorize.GET("/publish/:prefix/:distribution/sources", apiPublishListChanges)
authorize.PUT("/publish/:prefix/:distribution/sources", apiPublishSetSources)
authorize.DELETE("/publish/:prefix/:distribution/sources", apiPublishDropChanges)
authorize.PUT("/publish/:prefix/:distribution/sources/:component", apiPublishUpdateSource)
authorize.DELETE("/publish/:prefix/:distribution/sources/:component", apiPublishRemoveSource)
authorize.POST("/publish/:prefix/:distribution/update", apiPublishUpdate)
}
{
api.GET("/snapshots", apiSnapshotsList)
api.POST("/snapshots", apiSnapshotsCreate)
api.PUT("/snapshots/:name", apiSnapshotsUpdate)
api.GET("/snapshots/:name", apiSnapshotsShow)
api.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
api.DELETE("/snapshots/:name", apiSnapshotsDrop)
api.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
api.POST("/snapshots/:name/merge", apiSnapshotsMerge)
api.POST("/snapshots/:name/pull", apiSnapshotsPull)
authorize.GET("/snapshots", apiSnapshotsList)
authorize.POST("/snapshots", apiSnapshotsCreate)
authorize.PUT("/snapshots/:name", apiSnapshotsUpdate)
authorize.GET("/snapshots/:name", apiSnapshotsShow)
authorize.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
authorize.DELETE("/snapshots/:name", apiSnapshotsDrop)
authorize.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
authorize.POST("/snapshots/:name/merge", apiSnapshotsMerge)
authorize.POST("/snapshots/:name/pull", apiSnapshotsPull)
}
{
api.GET("/packages/:key", apiPackagesShow)
api.GET("/packages", apiPackages)
authorize.GET("/packages/:key", apiPackagesShow)
authorize.GET("/packages", apiPackages)
}
{
api.GET("/graph.:ext", apiGraph)
authorize.GET("/graph.:ext", apiGraph)
}
{
api.POST("/db/cleanup", apiDbCleanup)
authorize.POST("/db/cleanup", apiDbCleanup)
}
{
api.GET("/tasks", apiTasksList)
api.POST("/tasks-clear", apiTasksClear)
api.GET("/tasks-wait", apiTasksWait)
api.GET("/tasks/:id/wait", apiTasksWaitForTaskByID)
api.GET("/tasks/:id/output", apiTasksOutputShow)
api.GET("/tasks/:id/detail", apiTasksDetailShow)
api.GET("/tasks/:id/return_value", apiTasksReturnValueShow)
api.GET("/tasks/:id", apiTasksShow)
api.DELETE("/tasks/:id", apiTasksDelete)
authorize.GET("/tasks", apiTasksList)
authorize.POST("/tasks-clear", apiTasksClear)
authorize.GET("/tasks-wait", apiTasksWait)
authorize.GET("/tasks/:id/wait", apiTasksWaitForTaskByID)
authorize.GET("/tasks/:id/output", apiTasksOutputShow)
authorize.GET("/tasks/:id/detail", apiTasksDetailShow)
authorize.GET("/tasks/:id/return_value", apiTasksReturnValueShow)
authorize.GET("/tasks/:id", apiTasksShow)
authorize.DELETE("/tasks/:id", apiTasksDelete)
}
return router
+6
View File
@@ -251,6 +251,12 @@ func apiSnapshotsCreateFromRepository(c *gin.Context) {
return
}
err = CheckGroup(c, repo.LdapGroup)
if err != nil {
c.AbortWithError(403, err)
return
}
// including snapshot resource key
resources := []string{string(repo.Key()), "S" + b.Name}
taskName := fmt.Sprintf("Create snapshot of repo %s", name)