diff --git a/pkg/app/api/httpapi/callback.go b/pkg/app/api/httpapi/callback.go index 1343c4bb58..a84ae4843a 100644 --- a/pkg/app/api/httpapi/callback.go +++ b/pkg/app/api/httpapi/callback.go @@ -77,7 +77,7 @@ func (h *authHandler) handleCallback(w http.ResponseWriter, r *http.Request) { } } - user, err := getUser(ctx, sso, proj.Rbac, proj.Id, authCode) + user, err := getUser(ctx, sso, proj.Rbac, proj, authCode) if err != nil { h.handleError(w, r, "Unable to find user", err) return @@ -131,13 +131,13 @@ func checkState(r *http.Request, key string) error { return nil } -func getUser(ctx context.Context, sso *model.ProjectSSOConfig, rbac *model.ProjectRBACConfig, projectID, code string) (*model.User, error) { +func getUser(ctx context.Context, sso *model.ProjectSSOConfig, rbac *model.ProjectRBACConfig, project *model.Project, code string) (*model.User, error) { switch sso.Provider { case model.ProjectSSOConfig_GITHUB, model.ProjectSSOConfig_GITHUB_ENTERPRISE: if sso.Github == nil { return nil, fmt.Errorf("missing GitHub oauth in the SSO configuration") } - cli, err := github.NewOAuthClient(ctx, sso.Github, rbac, projectID, code) + cli, err := github.NewOAuthClient(ctx, sso.Github, rbac, project, code) if err != nil { return nil, err } diff --git a/pkg/app/ops/handler/handler.go b/pkg/app/ops/handler/handler.go index 49c9b35714..20d6575320 100644 --- a/pkg/app/ops/handler/handler.go +++ b/pkg/app/ops/handler/handler.go @@ -164,9 +164,10 @@ func (h *Handler) handleAddProject(w http.ResponseWriter, r *http.Request) { } var ( - id = r.FormValue("ID") - description = r.FormValue("Description") - sharedSSOName = r.FormValue("SharedSSO") + id = r.FormValue("ID") + description = r.FormValue("Description") + sharedSSOName = r.FormValue("SharedSSO") + allowStrayAsViewer = r.FormValue("AllowStrayAsViewer") == "on" ) if id == "" { http.Error(w, "invalid id", http.StatusBadRequest) @@ -188,9 +189,10 @@ func (h *Handler) handleAddProject(w http.ResponseWriter, r *http.Request) { var ( project = &model.Project{ - Id: id, - Desc: description, - SharedSsoName: sharedSSOName, + Id: id, + Desc: description, + SharedSsoName: sharedSSOName, + AllowStrayAsViewer: allowStrayAsViewer, } username = model.GenerateRandomString(10) password = model.GenerateRandomString(30) diff --git a/pkg/app/ops/handler/templates/AddProject b/pkg/app/ops/handler/templates/AddProject index 85f18f4793..0882297cc0 100644 --- a/pkg/app/ops/handler/templates/AddProject +++ b/pkg/app/ops/handler/templates/AddProject @@ -22,6 +22,8 @@ label {



+ +

diff --git a/pkg/app/web/src/__fixtures__/dummy-project.ts b/pkg/app/web/src/__fixtures__/dummy-project.ts index c710d83195..64e9d5bcfa 100644 --- a/pkg/app/web/src/__fixtures__/dummy-project.ts +++ b/pkg/app/web/src/__fixtures__/dummy-project.ts @@ -19,6 +19,7 @@ export const dummyProject: Project.AsObject = { createdAt: createdAt.unix(), updatedAt: updatedAt.unix(), staticAdminDisabled: false, + allowStrayAsViewer: false, rbac: { admin: "admin-team", editor: "editor-team", @@ -38,6 +39,7 @@ export function createProjectFromObject(o: Project.AsObject): Project { project.setCreatedAt(o.createdAt); project.setUpdatedAt(o.updatedAt); project.setStaticAdminDisabled(o.staticAdminDisabled); + project.setAllowStrayAsViewer(o.allowStrayAsViewer); if (o.rbac) { const rbac = new ProjectRBACConfig(); rbac.setAdmin(o.rbac.admin); diff --git a/pkg/model/project.proto b/pkg/model/project.proto index a63ce9fedf..9fba20668c 100644 --- a/pkg/model/project.proto +++ b/pkg/model/project.proto @@ -42,6 +42,10 @@ message Project { // It will be enabled when this parameter has no empty value. string shared_sso_name = 7; + // Enable this field will allow users not belonging + // to any registered teams to log in with Viewer role. + bool allow_stray_as_viewer = 8; + // Unix time when the project is created. int64 created_at = 14 [(validate.rules).int64.gt = 0]; // Unix time of the last time when the project is updated. diff --git a/pkg/oauth/github/github.go b/pkg/oauth/github/github.go index 1827380723..841ed73d11 100644 --- a/pkg/oauth/github/github.go +++ b/pkg/oauth/github/github.go @@ -31,7 +31,8 @@ import ( type OAuthClient struct { *github.Client - projectID string + project *model.Project + adminTeam string editorTeam string viewerTeam string @@ -41,10 +42,11 @@ type OAuthClient struct { func NewOAuthClient(ctx context.Context, sso *model.ProjectSSOConfig_GitHub, rbac *model.ProjectRBACConfig, - projectID, code string, + project *model.Project, + code string, ) (*OAuthClient, error) { c := &OAuthClient{ - projectID: projectID, + project: project, adminTeam: rbac.Admin, editorTeam: rbac.Editor, viewerTeam: rbac.Viewer, @@ -115,7 +117,7 @@ func (c *OAuthClient) GetUser(ctx context.Context) (*model.User, error) { Username: user.GetLogin(), AvatarUrl: user.GetAvatarURL(), Role: &model.Role{ - ProjectId: c.projectID, + ProjectId: c.project.Id, ProjectRole: role, }, }, nil @@ -151,6 +153,14 @@ func (c *OAuthClient) decideRole(user string, teams []*github.Team) (role model. return } + // In case the current user does not belong to any registered + // teams, if AllowStrayAsViewer option is set, assign Viewer role + // as user's role. + if c.project.AllowStrayAsViewer { + role = model.Role_VIEWER + return + } + err = fmt.Errorf("user (%s) not found in any of the %d project teams", user, len(teams)) return } diff --git a/pkg/oauth/github/github_test.go b/pkg/oauth/github/github_test.go index 915befc1d6..864155d461 100644 --- a/pkg/oauth/github/github_test.go +++ b/pkg/oauth/github/github_test.go @@ -29,6 +29,7 @@ func TestDecideRole(t *testing.T) { cases := []struct { name string username string + oc *OAuthClient teams []*github.Team role model.Role_ProjectRole wantErr bool @@ -36,6 +37,14 @@ func TestDecideRole(t *testing.T) { { name: "nothing", username: "foo", + oc: &OAuthClient{ + adminTeam: "org/team-admin", + editorTeam: "org/team-editor", + viewerTeam: "org/team-viewer", + project: &model.Project{ + AllowStrayAsViewer: false, + }, + }, teams: []*github.Team{ { Organization: &github.Organization{Login: stringPointer("org")}, @@ -44,9 +53,34 @@ func TestDecideRole(t *testing.T) { }, wantErr: true, }, + { + name: "viewer as default", + username: "foo", + oc: &OAuthClient{ + adminTeam: "org/team-admin", + editorTeam: "org/team-editor", + viewerTeam: "org/team-viewer", + project: &model.Project{ + AllowStrayAsViewer: true, + }, + }, + teams: []*github.Team{ + { + Organization: &github.Organization{Login: stringPointer("org")}, + Slug: stringPointer("team1"), + }, + }, + role: model.Role_VIEWER, + wantErr: false, + }, { name: "admin", username: "foo", + oc: &OAuthClient{ + adminTeam: "org/team-admin", + editorTeam: "org/team-editor", + viewerTeam: "org/team-viewer", + }, teams: []*github.Team{ { Organization: &github.Organization{Login: stringPointer("org")}, @@ -66,6 +100,11 @@ func TestDecideRole(t *testing.T) { { name: "editor", username: "foo", + oc: &OAuthClient{ + adminTeam: "org/team-admin", + editorTeam: "org/team-editor", + viewerTeam: "org/team-viewer", + }, teams: []*github.Team{ { Organization: &github.Organization{Login: stringPointer("org")}, @@ -85,6 +124,11 @@ func TestDecideRole(t *testing.T) { { name: "viewer", username: "foo", + oc: &OAuthClient{ + adminTeam: "org/team-admin", + editorTeam: "org/team-editor", + viewerTeam: "org/team-viewer", + }, teams: []*github.Team{ { Organization: &github.Organization{Login: stringPointer("org")}, @@ -103,14 +147,9 @@ func TestDecideRole(t *testing.T) { }, } - oc := &OAuthClient{ - adminTeam: "org/team-admin", - editorTeam: "org/team-editor", - viewerTeam: "org/team-viewer", - } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - role, err := oc.decideRole(tc.username, tc.teams) + role, err := tc.oc.decideRole(tc.username, tc.teams) assert.Equal(t, tc.wantErr, err != nil) if err == nil { assert.Equal(t, tc.role, role)