Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type (
ServerProxyProto string `default:"https" split_words:"true"`

OrganizationEntries []string `split_words:"true"`
MemberEntries []string `split_words:"true"`
AdminUsers []string `split_words:"true"`

License string `split_words:"true"`
Expand Down
15 changes: 8 additions & 7 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,14 @@ func newChatConfig(c *Config) *server.ChatConfig {
func NewInteractor(c *Config) server.Interactor {
return interactor.NewInteractor(
&interactor.InteractorConfig{
ServerHost: c.ServerHost,
ServerProto: c.ServerProto,
OrgEntries: c.OrganizationEntries,
AdminUsers: c.AdminUsers,
LicenseKey: c.License,
Store: newStore(c),
SCM: newSCM(c),
ServerHost: c.ServerHost,
ServerProto: c.ServerProto,
OrgEntries: c.OrganizationEntries,
MemberEntries: c.MemberEntries,
AdminUsers: c.AdminUsers,
LicenseKey: c.License,
Store: newStore(c),
SCM: newSCM(c),
},
)
}
Expand Down
7 changes: 7 additions & 0 deletions docs/references/GITPLOY_MEMBER_ENTRIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# GITPLOY_MEMBER_ENTRIES

Optional comma-separated list of accounts, used to limit to users in this list, or users that are members of organizations included in this list.

```
GITPLOY_MEMBER_ENTRIES=octocat,gitploy-io
```
1 change: 1 addition & 0 deletions docs/references/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Index of server configuration settings:
* [GITPLOY_PROXY_SERVER_PROTO](./GITPLOY_PROXY_SERVER_PROTO.md)
* [GITPLOY_WEBHOOK_SECRET](./GITPLOY_WEBHOOK_SECRET.md)
* [GITPLOY_ORGANIZATION_ENTRIES](./GITPLOY_ORGANIZATION_ENTRIES.md)
* [GITPLOY_MEMBER_ENTRIES](./GITPLOY_MEMBER_ENTRIES.md)
* [GITPLOY_ADMIN_USERS](./GITPLOY_ADMIN_USERS.md)
* [GITPLOY_GITHUB_CLIENT_ID](./GITPLOY_GITHUB_CLIENT_ID.md)
* [GITPLOY_GITHUB_CLIENT_SECRET](./GITPLOY_GITHUB_CLIENT_SECRET.md)
Expand Down
1 change: 1 addition & 0 deletions docs/references/deploy.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Field |Type |Required |Description
`description` |*string* |`false` |This field is the short description of the deployment.
`auto_merge` |*boolean* |`false` |This field is used to ensure that the requested ref is not behind the repository's default branch. If you deploy with the commit or the tag you need to set `false`. For rollback, Gitploy set the field `false`.
`required_contexts` |*[]string* |`false` |This field allows you to specify a subset of contexts that must be success.
`payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment.
`production_environment` |*boolean* |`false` |This field specifies whether this runtime environment is production or not.
`approval` |*[Approval](#approval)* |`false` |This field configures approval.

Expand Down
29 changes: 16 additions & 13 deletions internal/interactor/interactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ type (
ServerHost string
ServerProto string

orgEntries []string
orgEntries []string
memberEntries []string
// Admin Users
admins []string

Expand All @@ -32,8 +33,9 @@ type (
ServerHost string
ServerProto string

OrgEntries []string
AdminUsers []string
OrgEntries []string
MemberEntries []string
AdminUsers []string

LicenseKey string

Expand All @@ -44,16 +46,17 @@ type (

func NewInteractor(c *InteractorConfig) *Interactor {
i := &Interactor{
ServerHost: c.ServerHost,
ServerProto: c.ServerProto,
orgEntries: c.OrgEntries,
admins: c.AdminUsers,
licenseKey: c.LicenseKey,
Store: c.Store,
SCM: c.SCM,
stopCh: make(chan struct{}),
events: evbus.New(),
log: zap.L().Named("interactor"),
ServerHost: c.ServerHost,
ServerProto: c.ServerProto,
orgEntries: c.OrgEntries,
memberEntries: c.MemberEntries,
admins: c.AdminUsers,
licenseKey: c.LicenseKey,
Store: c.Store,
SCM: c.SCM,
stopCh: make(chan struct{}),
events: evbus.New(),
log: zap.L().Named("interactor"),
}

go func() {
Expand Down
3 changes: 2 additions & 1 deletion internal/interactor/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ type (
}

SCM interface {
GetUser(ctx context.Context, token string) (*vo.RemoteUser, error)
GetRemoteUserByToken(ctx context.Context, token string) (*vo.RemoteUser, error)
ListRemoteOrgsByToken(ctx context.Context, token string) ([]string, error)

ListRemoteRepos(ctx context.Context, u *ent.User) ([]*vo.RemoteRepo, error)

Expand Down
43 changes: 29 additions & 14 deletions internal/interactor/mock/pkg.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 26 additions & 6 deletions internal/interactor/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@ package interactor

import (
"context"

"github.com/gitploy-io/gitploy/vo"
)

func (i *Interactor) GetRemoteUserByToken(ctx context.Context, token string) (*vo.RemoteUser, error) {
return i.SCM.GetUser(ctx, token)
}

func (i *Interactor) IsAdminUser(ctx context.Context, login string) bool {
for _, admin := range i.admins {
if login == admin {
Expand All @@ -19,3 +13,29 @@ func (i *Interactor) IsAdminUser(ctx context.Context, login string) bool {

return false
}

func (i *Interactor) IsEntryMember(ctx context.Context, login string) bool {
if i.memberEntries == nil {
return true
}

for _, m := range i.memberEntries {
if login == m {
return true
}
}

return false
}

func (i *Interactor) IsOrgMember(ctx context.Context, orgs []string) bool {
for _, o := range orgs {
for _, entry := range i.memberEntries {
if o == entry {
return true
}
}
}

return false
}
48 changes: 48 additions & 0 deletions internal/interactor/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,51 @@ func TestInteractor_IsAdminUser(t *testing.T) {
}
})
}

func TestInteractor_IsEntryMember(t *testing.T) {
t.Run("Return false when the user's login is not included.", func(t *testing.T) {
i := &Interactor{
memberEntries: []string{"octocat"},
}

want := false
if ret := i.IsEntryMember(context.Background(), "coco"); ret != want {
t.Fatalf("IsEntryMember = %v, wanted %v", ret, want)
}
})

t.Run("Return true when the user's login is included.", func(t *testing.T) {
i := &Interactor{
memberEntries: []string{"octocat"},
}

want := true
if ret := i.IsEntryMember(context.Background(), "octocat"); ret != want {
t.Fatalf("IsEntryMember = %v, wanted %v", ret, want)
}
})
}

func TestInteractor_IsOrgMember(t *testing.T) {
t.Run("Return false when the org is not included.", func(t *testing.T) {
i := &Interactor{
memberEntries: []string{"gitploy-io"},
}

want := false
if ret := i.IsOrgMember(context.Background(), []string{"github"}); ret != want {
t.Fatalf("IsEntryMember = %v, wanted %v", ret, want)
}
})

t.Run("Return true when the org is included.", func(t *testing.T) {
i := &Interactor{
memberEntries: []string{"gitploy-io"},
}

want := true
if ret := i.IsOrgMember(context.Background(), []string{"gitploy-io"}); ret != want {
t.Fatalf("IsEntryMember = %v, wanted %v", ret, want)
}
})
}
20 changes: 19 additions & 1 deletion internal/pkg/github/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"context"

"github.com/gitploy-io/gitploy/vo"
"github.com/google/go-github/v32/github"
)

func (g *Github) GetUser(ctx context.Context, token string) (*vo.RemoteUser, error) {
func (g *Github) GetRemoteUserByToken(ctx context.Context, token string) (*vo.RemoteUser, error) {
c := g.Client(ctx, token)

u, _, err := c.Users.Get(ctx, "")
Expand All @@ -16,3 +17,20 @@ func (g *Github) GetUser(ctx context.Context, token string) (*vo.RemoteUser, err

return mapGithubUserToUser(u), err
}

func (g *Github) ListRemoteOrgsByToken(ctx context.Context, token string) ([]string, error) {
// TODO: List all orgs.
orgs, _, err := g.Client(ctx, token).
Organizations.
List(ctx, "", &github.ListOptions{})
if err != nil {
return nil, err
}

ret := []string{}
for _, o := range orgs {
ret = append(ret, *o.Login)
}

return ret, nil
}
35 changes: 26 additions & 9 deletions internal/server/web/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,43 @@ func (w *Web) Signin(c *gin.Context) {

t, err := w.c.Exchange(c, code)
if err != nil {
w.log.Error("failed to exchange the code.", zap.Error(err))
c.String(http.StatusInternalServerError, "There is an issue to exchange the code.")
w.log.Error("It has failed to exchange the code.", zap.Error(err))
c.String(http.StatusUnauthorized, "There is an issue to exchange the code.")
return
}

if !t.Valid() {
w.log.Error("invalid token.", zap.Error(err))
c.String(http.StatusInternalServerError, "It's a invalid token.")
c.String(http.StatusUnauthorized, "It's a invalid token.")
return
}

ctx := c.Request.Context()

ru, err := w.i.GetRemoteUserByToken(ctx, t.AccessToken)
if err != nil {
w.log.Error("failed to fetch a user from SCM.", zap.Error(err))
c.String(http.StatusInternalServerError, "It has failed to fetch a user from SCM.")
w.log.Error("It has failed to get the remote user.", zap.Error(err))
c.String(http.StatusInternalServerError, "It has failed to get the remote user.")
return
}
w.log.Debug("Get user's login.", zap.String("login", ru.Login))

orgs, err := w.i.ListRemoteOrgsByToken(ctx, t.AccessToken)
if err != nil {
w.log.Error("It has failed to list remote orgs.", zap.Error(err))
c.String(http.StatusInternalServerError, "It has failed to list remote orgs.")
return
}
w.log.Debug("List remote orgs.", zap.Strings("orgs", orgs))

// Check the login of user who is member and admin.
if !(w.i.IsEntryMember(ctx, ru.Login) || w.i.IsOrgMember(ctx, orgs)) {
w.log.Warn("This login not a member of an approved organization.", zap.String("login", ru.Login))
c.String(http.StatusUnauthorized, "You are not a member of an approved organization.")
return
}

admin := w.i.IsAdminUser(ctx, ru.Login)

// Synchronize from the remote user. It synchronizes
// user information and save generated OAuth token.
Expand All @@ -113,7 +131,7 @@ func (w *Web) Signin(c *gin.Context) {
Token: t.AccessToken,
Refresh: t.RefreshToken,
Expiry: t.Expiry,
Admin: w.i.IsAdminUser(ctx, ru.Login),
Admin: admin,
}

if _, err = w.i.FindUserByID(ctx, u.ID); ent.IsNotFound(err) {
Expand All @@ -124,13 +142,12 @@ func (w *Web) Signin(c *gin.Context) {
return
}

if lic.IsOverLimit() {
w.log.Error("There are no more seats.", zap.Error(err))
if lic.MemberCount >= lic.MemberLimit {
w.log.Warn("There are no more seats. It prevents to over the limit.", zap.Int("member_count", lic.MemberCount), zap.Int("member_limit", lic.MemberLimit))
c.String(http.StatusPaymentRequired, "There are no more seats.")
return
}

w.log.Debug("Check the count of member.", zap.Int("member_count", lic.MemberCount), zap.Int("member_limit", lic.MemberLimit))
u, _ = w.i.CreateUser(ctx, u)
} else if err != nil {
w.log.Error("It failed to save the user.", zap.Error(err))
Expand Down
Loading