From 2715a55620d1eb77e686a9cff57404652f09923f Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 12 Oct 2022 15:11:45 +0200 Subject: [PATCH 01/38] introduce functionality to assign/create team via group claim --- pkg/models/error.go | 22 ++++++++++ pkg/models/teams.go | 61 ++++++++++++++++++++++++++- pkg/modules/auth/openid/openid.go | 69 ++++++++++++++++++++++++++++--- 3 files changed, 146 insertions(+), 6 deletions(-) diff --git a/pkg/models/error.go b/pkg/models/error.go index 580ba0551..c63ee71eb 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1095,6 +1095,28 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."} } +type ErrTeamsDoNotExist struct { + Name string +} + +// IsErrTeamDoNotExist checks if an error is ErrTeamDoesNotExist. +func IsErrTeamsDoNotExist(err error) bool { + _, ok := err.(ErrTeamsDoNotExist) + return ok +} + +func (err ErrTeamsDoNotExist) Error() string { + return fmt.Sprintf("Team does not exist [Team Name: %v]", err.Name) +} + +// ErrCodeTeamDoesNotExist holds the unique world-error code of this error +const ErrCodeTeamsDoNotExist = 6002 + +// HTTPError holds the http error description +func (err ErrTeamsDoNotExist) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No team with given name exists."} +} + // ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace type ErrTeamAlreadyHasAccess struct { TeamID int64 diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 0b21a5bd9..1b1d09c0f 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -79,7 +79,7 @@ type TeamMember struct { } // TableName makes beautiful table names -func (*TeamMember) TableName() string { +func (TeamMember) TableName() string { return "team_members" } @@ -119,6 +119,34 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) { return } +func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { + if name == "" { + return teams, ErrTeamsDoNotExist{name} + } + + var ts []*Team + + exists := s. + Where("name = ?", name). + Find(&ts) + if exists != nil { + return + } + if len(ts) == 0 { + return ts, ErrTeamsDoNotExist{name} + } + + // //for each ts + // teamSlice := []*Team{ts} + // err = addMoreInfoToTeams(s, teamSlice) + // if err != nil { + // return + // } + + teams = ts + + return +} func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) { @@ -282,6 +310,37 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) { }) } +func (t *Team) CreateNoAdmin(s *xorm.Session, a web.Auth) (err error) { + doer, err := user.GetFromAuth(a) + if err != nil { + return err + } + + // Check if we have a name + if t.Name == "" { + return ErrTeamNameCannotBeEmpty{} + } + + t.CreatedByID = doer.ID + t.CreatedBy = doer + + _, err = s.Insert(t) + if err != nil { + return + } + + // Insert the current user as member and admin + tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: false} + if err = tm.Create(s, doer); err != nil { + return err + } + + return events.Dispatch(&TeamCreatedEvent{ + Team: t, + Doer: a, + }) +} + // Delete deletes a team // @Summary Deletes a team // @Description Delets a team. This will also remove the access for all users in that team. diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 9509e52ea..95148db89 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -53,16 +53,18 @@ type Provider struct { AuthURL string `json:"auth_url"` LogoutURL string `json:"logout_url"` ClientID string `json:"client_id"` + Scope string `json:"scope"` ClientSecret string `json:"-"` openIDProvider *oidc.Provider Oauth2Config *oauth2.Config `json:"-"` } type claims struct { - Email string `json:"email"` - Name string `json:"name"` - PreferredUsername string `json:"preferred_username"` - Nickname string `json:"nickname"` + Email string `json:"email"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Nickname string `json:"nickname"` + Group []string `json:"groups"` } func init() { @@ -189,22 +191,79 @@ func HandleCallback(c echo.Context) error { // Check if we have seen this user before u, err := getOrCreateUser(s, cl, idToken.Issuer, idToken.Subject) + + log.Errorf("Issuer %s: %v", idToken.Issuer, err) + if err != nil { _ = s.Rollback() log.Errorf("Error creating new user for provider %s: %v", provider.Name, err) return handler.HandleHTTPError(err, c) } + // Check if we have seen this user before + teams, err := GetOrCreateTeamsByNames(s, cl.Group, u) + if err != nil { + log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) + return err + } else { + for _, team := range teams { + tm := models.TeamMember{TeamID: team.ID, Username: u.Username} + if err = tm.Create(s, u); err != nil { + switch t := err.(type) { + case *models.ErrUserIsMemberOfTeam: + log.Errorf("ErrUserIsMemberOfTeam", t) + break + default: + log.Errorf("Error assigning User to team", t) + } + } + } + } + err = s.Commit() if err != nil { return handler.HandleHTTPError(err, c) } - // Create token return auth.NewUserAuthTokenResponse(u, c, false) } +func GetOrCreateTeamsByNames(s *xorm.Session, teamNames []string, u *user.User) (te []models.Team, err error) { + te = []models.Team{} + for _, t := range teamNames { + team, err := models.GetTeamsByName(s, t) + + if models.IsErrTeamsDoNotExist(err) { + log.Errorf("No such Team: %v, got %v", t, team, err) + tea := &models.Team{ + Name: t, + } + err := tea.CreateNoAdmin(s, u) + if err != nil { + log.Errorf("Teams: %v, err: %v", tea, err) + } else { + te = append(te, *tea) + } + } else { + // if multiple teams with same name are found, + if len(team) == 1 { + te = append(te, *team[len(team)-1]) + } else { + log.Errorf("Multiple Teams have the same name: %v, ", team[len(team)-1].Name) + } + } + } + return te, err +} + +// assign user to team +// remove user from team if not in group +// if multiple teams found with same name -> do nothing +// optional: assign by id +// + func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *user.User, err error) { + // Check if the user exists for that issuer and subject u, err = user.GetUserWithEmail(s, &user.User{ Issuer: issuer, From 7366fa996e75d086b53ae80017030b289e59b64d Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 7 Dec 2022 15:32:58 +0100 Subject: [PATCH 02/38] wip assign groups via oidc --- pkg/models/team_members.go | 22 ++++++++++++++- pkg/models/teams.go | 45 +++++-------------------------- pkg/modules/auth/openid/openid.go | 40 +++++++++++++-------------- 3 files changed, 46 insertions(+), 61 deletions(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index f236f5f97..bbcc06673 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -18,6 +18,7 @@ package models import ( "code.vikunja.io/api/pkg/events" + "code.vikunja.io/api/pkg/log" user2 "code.vikunja.io/api/pkg/user" "code.vikunja.io/web" "xorm.io/xorm" @@ -54,7 +55,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) { // Check if that user is already part of the team exists, err := s. - Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). + Where("team_id = ? AND user_name = ?", tm.TeamID, tm.UserID). Get(&TeamMember{}) if err != nil { return @@ -109,6 +110,25 @@ func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) { return } +func (tm *TeamMember) CheckMembership(s *xorm.Session) (err error) { + member, err := user2.GetUserByUsername(s, tm.Username) + if err != nil { + return + } + tm.UserID = member.ID + exists, err := s. + Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). + Get(&TeamMember{}) + if err != nil { + return + } + if exists { + log.Errorf("Team member already exists %v", ErrUserIsMemberOfTeam{tm.UserID, tm.UserID}) + return ErrUserIsMemberOfTeam{tm.UserID, tm.UserID} + } + return +} + // Update toggles a team member's admin status // @Summary Toggle a team member's admin status // @Description If a user is team admin, this will make them member and vise-versa. diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 1b1d09c0f..2a13b0a66 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -119,6 +119,8 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) { return } + +// GetTeamByID gets teams by name func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { if name == "" { return teams, ErrTeamsDoNotExist{name} @@ -135,14 +137,6 @@ func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { if len(ts) == 0 { return ts, ErrTeamsDoNotExist{name} } - - // //for each ts - // teamSlice := []*Team{ts} - // err = addMoreInfoToTeams(s, teamSlice) - // if err != nil { - // return - // } - teams = ts return @@ -298,8 +292,9 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) { return } - // Insert the current user as member and admin - tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: true} + var admin bool = true + // } + tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: admin} if err = tm.Create(s, doer); err != nil { return err } @@ -310,35 +305,9 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) { }) } -func (t *Team) CreateNoAdmin(s *xorm.Session, a web.Auth) (err error) { - doer, err := user.GetFromAuth(a) - if err != nil { - return err - } - - // Check if we have a name - if t.Name == "" { - return ErrTeamNameCannotBeEmpty{} - } - - t.CreatedByID = doer.ID - t.CreatedBy = doer - - _, err = s.Insert(t) - if err != nil { - return - } - +func (t *Team) ManageAdminRight(teamMember TeamMember, admin bool) { // Insert the current user as member and admin - tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: false} - if err = tm.Create(s, doer); err != nil { - return err - } - - return events.Dispatch(&TeamCreatedEvent{ - Team: t, - Doer: a, - }) + teamMember.Admin = admin } // Delete deletes a team diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 95148db89..4f894e538 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -64,7 +64,7 @@ type claims struct { Name string `json:"name"` PreferredUsername string `json:"preferred_username"` Nickname string `json:"nickname"` - Group []string `json:"groups"` + Teams []string `json:"groups"` } func init() { @@ -192,34 +192,28 @@ func HandleCallback(c echo.Context) error { // Check if we have seen this user before u, err := getOrCreateUser(s, cl, idToken.Issuer, idToken.Subject) - log.Errorf("Issuer %s: %v", idToken.Issuer, err) - if err != nil { _ = s.Rollback() log.Errorf("Error creating new user for provider %s: %v", provider.Name, err) return handler.HandleHTTPError(err, c) } - // Check if we have seen this user before - teams, err := GetOrCreateTeamsByNames(s, cl.Group, u) - if err != nil { - log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) - return err - } else { - for _, team := range teams { - tm := models.TeamMember{TeamID: team.ID, Username: u.Username} - if err = tm.Create(s, u); err != nil { - switch t := err.(type) { - case *models.ErrUserIsMemberOfTeam: - log.Errorf("ErrUserIsMemberOfTeam", t) - break - default: - log.Errorf("Error assigning User to team", t) + // Check if we have seen these teams before + if len(cl.Teams) > 0 { + teams, err := GetOrCreateTeamsByNames(s, cl.Teams, u) + if err != nil { + log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) + return err + } else { + for _, team := range teams { + tm := models.TeamMember{TeamID: team.ID, Username: u.Username} + err := tm.CheckMembership(s) + if err == nil { + tm.Create(s, u) } } } } - err = s.Commit() if err != nil { return handler.HandleHTTPError(err, c) @@ -233,12 +227,13 @@ func GetOrCreateTeamsByNames(s *xorm.Session, teamNames []string, u *user.User) for _, t := range teamNames { team, err := models.GetTeamsByName(s, t) + // if team does not exists, create it if models.IsErrTeamsDoNotExist(err) { - log.Errorf("No such Team: %v, got %v", t, team, err) + log.Debugf("No such Team: %v, create %v ..", t, team) tea := &models.Team{ Name: t, } - err := tea.CreateNoAdmin(s, u) + err := tea.Create(s, u) if err != nil { log.Errorf("Teams: %v, err: %v", tea, err) } else { @@ -247,9 +242,10 @@ func GetOrCreateTeamsByNames(s *xorm.Session, teamNames []string, u *user.User) } else { // if multiple teams with same name are found, if len(team) == 1 { + // append team to return value te = append(te, *team[len(team)-1]) } else { - log.Errorf("Multiple Teams have the same name: %v, ", team[len(team)-1].Name) + log.Debugf("Multiple Teams have the same name: %v, ignore assignment of %v", team[len(team)-1].Name, u.Name) } } } From 83fdd06aacc8bd0107fdd1500476fc3d75283002 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:13:33 +0100 Subject: [PATCH 03/38] migration to add idcID to teams --- pkg/migration/20230104152903.go | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 pkg/migration/20230104152903.go diff --git a/pkg/migration/20230104152903.go b/pkg/migration/20230104152903.go new file mode 100644 index 000000000..8991ddd89 --- /dev/null +++ b/pkg/migration/20230104152903.go @@ -0,0 +1,43 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type teams20230104152903 struct { + OidcID string `xorm:"varchar(250) null" maxLength:"250" json:"oidc_id"` +} + +func (teams20230104152903) TableName() string { + return "teams" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20230104152903", + Description: "Adding OidcID to teams", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(teams20230104152903{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} From ae8e3d91089feafafbc03c11894068f94e77b602 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:22:42 +0100 Subject: [PATCH 04/38] add errors to user --- pkg/user/error.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/user/error.go b/pkg/user/error.go index 88c4f5766..0f0defe78 100644 --- a/pkg/user/error.go +++ b/pkg/user/error.go @@ -426,6 +426,32 @@ func (err *ErrNoOpenIDEmailProvided) HTTPError() web.HTTPError { } } +// ErrNoOpenIDEmailProvided represents a "NoEmailProvided" kind of error. +type ErrOpenIDCustomScopeMalformed struct { +} + +// IsErrNoEmailProvided checks if an error is a ErrNoOpenIDEmailProvided. +func IsErrOpenIDCustomScopeMalformed(err error) bool { + _, ok := err.(*ErrOpenIDCustomScopeMalformed) + return ok +} + +func (err *ErrOpenIDCustomScopeMalformed) Error() string { + return "Custom Scope malformed" +} + +// ErrCodeNoOpenIDEmailProvided holds the unique world-error code of this error +const ErrCodeOpenIDCustomScopeMalformed = 1022 + +// HTTPError holds the http error description +func (err *ErrOpenIDCustomScopeMalformed) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusPreconditionFailed, + Code: ErrCodeOpenIDCustomScopeMalformed, + Message: "The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID", + } +} + // ErrAccountDisabled represents a "AccountDisabled" kind of error. type ErrAccountDisabled struct { UserID int64 From db1f1423bb1439eb6a94af077f0137a5dcce607a Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:31:43 +0100 Subject: [PATCH 05/38] add OidcIDto teams --- pkg/models/team_members.go | 15 +++++---------- pkg/models/teams.go | 3 +++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index bbcc06673..39e12c33b 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -18,7 +18,6 @@ package models import ( "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/log" user2 "code.vikunja.io/api/pkg/user" "code.vikunja.io/web" "xorm.io/xorm" @@ -55,7 +54,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) { // Check if that user is already part of the team exists, err := s. - Where("team_id = ? AND user_name = ?", tm.TeamID, tm.UserID). + Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). Get(&TeamMember{}) if err != nil { return @@ -110,23 +109,19 @@ func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) { return } -func (tm *TeamMember) CheckMembership(s *xorm.Session) (err error) { +func (tm *TeamMember) CheckMembership(s *xorm.Session) (exists bool, err error) { member, err := user2.GetUserByUsername(s, tm.Username) if err != nil { return } tm.UserID = member.ID - exists, err := s. + exists, err = s. Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). Get(&TeamMember{}) - if err != nil { + if exists { return } - if exists { - log.Errorf("Team member already exists %v", ErrUserIsMemberOfTeam{tm.UserID, tm.UserID}) - return ErrUserIsMemberOfTeam{tm.UserID, tm.UserID} - } - return + return exists, ErrUserIsMemberOfTeam{tm.UserID, tm.UserID} } // Update toggles a team member's admin status diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 2a13b0a66..a21fe8d79 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -20,6 +20,7 @@ import ( "time" "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/user" @@ -38,6 +39,8 @@ type Team struct { // The team's description. Description string `xorm:"longtext null" json:"description"` CreatedByID int64 `xorm:"bigint not null INDEX" json:"-"` + // The team's oidc id delivered by the oidc provider + OidcID string `xorm:"varchar(250) null" maxLength:"250" json:"oidc_id"` // The user who created this team. CreatedBy *user.User `xorm:"-" json:"created_by"` From 962e09174f395db63e17c56a3fa2570fe6b703a8 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:33:45 +0100 Subject: [PATCH 06/38] add functionality of searching teams by oidcId and name to teams.go --- pkg/models/teams.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index a21fe8d79..f0ecbf541 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -123,7 +123,7 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) { return } -// GetTeamByID gets teams by name +// GetTeamByName gets teams by name func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { if name == "" { return teams, ErrTeamsDoNotExist{name} @@ -131,18 +131,28 @@ func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { var ts []*Team - exists := s. + err = s. Where("name = ?", name). Find(&ts) - if exists != nil { - return - } - if len(ts) == 0 { + if err != nil || len(ts) == 0 { return ts, ErrTeamsDoNotExist{name} } teams = ts + return teams, err +} - return +// GetTeamByOidcIDAndName gets teams where oidc_id and name match parameters +// For oidc team creation oidcID and Name need to be set +func GetTeamByOidcIDAndName(s *xorm.Session, id string, name string) (team Team, err error) { + exists, err := s. + Table("teams"). + Where("oidc_id = ? AND name = ?", id, name). + Get(&team) + log.Debugf("GetTeamByOidcIDAndName: %v, exists: %v", team.Name, exists) + if exists && err == nil { + return team, nil + } + return team, ErrTeamsDoNotExist{id} } func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) { @@ -295,8 +305,7 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) { return } - var admin bool = true - // } + var admin = true tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: admin} if err = tm.Create(s, doer); err != nil { return err From 8b109fbcdadbd1de8f3b45e18ad2b9f90fd445c0 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:36:04 +0100 Subject: [PATCH 07/38] add scope to provider --- pkg/modules/auth/openid/providers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/modules/auth/openid/providers.go b/pkg/modules/auth/openid/providers.go index e260e9b3c..a96f822c8 100644 --- a/pkg/modules/auth/openid/providers.go +++ b/pkg/modules/auth/openid/providers.go @@ -132,6 +132,7 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro OriginalAuthURL: pi["authurl"].(string), ClientSecret: pi["clientsecret"].(string), LogoutURL: logoutURL, + Scope: pi["scope"].(string), } cl, is := pi["clientid"].(int) From 5061028f2074d41b6f9146d63a9fce61c269b0ae Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:40:35 +0100 Subject: [PATCH 08/38] add VikunjaGroups to claums struct in openid.go --- pkg/modules/auth/openid/openid.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 4f894e538..fefd21b74 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -58,13 +58,13 @@ type Provider struct { openIDProvider *oidc.Provider Oauth2Config *oauth2.Config `json:"-"` } - type claims struct { - Email string `json:"email"` - Name string `json:"name"` - PreferredUsername string `json:"preferred_username"` - Nickname string `json:"nickname"` - Teams []string `json:"groups"` + Email string `json:"email"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Nickname string `json:"nickname"` + Teams []string `json:"groups"` + VikunjaGroups interface{} `json:"vikunja_groups"` } func init() { From d3fa4a04c0e0aa39258e3a90cc6e55cff0a405b2 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:41:30 +0100 Subject: [PATCH 09/38] add TeamData struct to openid.go --- pkg/modules/auth/openid/openid.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index fefd21b74..3ede85ba6 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "math/rand" "net/http" "time" @@ -58,6 +59,11 @@ type Provider struct { openIDProvider *oidc.Provider Oauth2Config *oauth2.Config `json:"-"` } +type TeamData struct { + TeamName string + OidcID string + Description string +} type claims struct { Email string `json:"email"` Name string `json:"name"` From 3ef25e7be55cc357cfc9538d38a1b563a1838166 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:49:19 +0100 Subject: [PATCH 10/38] add functionality to assign user to teams through oidc custom claim --- pkg/modules/auth/openid/openid.go | 123 ++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 31 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 3ede85ba6..0335166d4 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -150,6 +150,7 @@ func HandleCallback(c echo.Context) error { // Extract custom claims cl := &claims{} + err = idToken.Claims(cl) if err != nil { log.Errorf("Error getting token claims for provider %s: %v", provider.Name, err) @@ -204,57 +205,117 @@ func HandleCallback(c echo.Context) error { return handler.HandleHTTPError(err, c) } - // Check if we have seen these teams before - if len(cl.Teams) > 0 { - teams, err := GetOrCreateTeamsByNames(s, cl.Teams, u) + // does the oidc token contain well formed "vikunja_groups" through vikunja_scope + teamData, err := getTeamDataFromToken(cl.VikunjaGroups, provider) + if err != nil { + log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) + return handler.HandleHTTPError(err, c) + } + // check if we have seen these teams before. + // find or create Teams and assign user as teammember. + if len(teamData) > 0 { + log.Debugf("TeamData is set %v", teamData) + teams, err := GetOrCreateTeamsByNames(s, teamData, u) if err != nil { log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) return err - } else { - for _, team := range teams { - tm := models.TeamMember{TeamID: team.ID, Username: u.Username} - err := tm.CheckMembership(s) - if err == nil { - tm.Create(s, u) + } + + for _, team := range teams { + tm := models.TeamMember{TeamID: team.ID, Username: u.Username} + exists, err := tm.CheckMembership(s) + if !exists { + err = tm.Create(s, u) + if err != nil { + log.Debugf("Could not assign %v to %v. %v", u.Username, team.Name, err) } + } else { + log.Debugf("Team exists? %v or error: %v", exists, err) } } } + err = s.Commit() if err != nil { + _ = s.Rollback() + log.Errorf("Error creating new Team for provider %s: %v", provider.Name, err) return handler.HandleHTTPError(err, c) } // Create token return auth.NewUserAuthTokenResponse(u, c, false) } -func GetOrCreateTeamsByNames(s *xorm.Session, teamNames []string, u *user.User) (te []models.Team, err error) { - te = []models.Team{} - for _, t := range teamNames { - team, err := models.GetTeamsByName(s, t) - - // if team does not exists, create it - if models.IsErrTeamsDoNotExist(err) { - log.Debugf("No such Team: %v, create %v ..", t, team) - tea := &models.Team{ - Name: t, +func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []TeamData, err error) { + teamData = []TeamData{} + if groups != nil { + el := groups.([]interface{}) + for _, data := range el { + team := data.(map[string]interface{}) + log.Debugf("%s", team) + var name string + var description string + var oidcID string + if team["name"] != nil { + name = team["name"].(string) } - err := tea.Create(s, u) - if err != nil { - log.Errorf("Teams: %v, err: %v", tea, err) - } else { - te = append(te, *tea) + if team["description"] != nil { + description = team["description"].(string) } - } else { - // if multiple teams with same name are found, - if len(team) == 1 { - // append team to return value - te = append(te, *team[len(team)-1]) - } else { - log.Debugf("Multiple Teams have the same name: %v, ignore assignment of %v", team[len(team)-1].Name, u.Name) + if team["oidcID"] != nil { + switch t := team["oidcID"].(type) { + case int64: + oidcID = fmt.Sprintf("%v", team["oidcID"]) + case string: + oidcID = string(team["oidcID"].(string)) + case float64: + fl := float64(team["oidcID"].(float64)) + oidcID = fmt.Sprintf("%v", fl) + default: + log.Errorf("No oidcID assigned for %v or type %v not supported", team, t) + } } + if name == "" || oidcID == "" { + log.Errorf("Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s", provider.Name) + return teamData, &user.ErrOpenIDCustomScopeMalformed{} + } + teamData = append(teamData, TeamData{TeamName: name, OidcID: oidcID, Description: description}) } } + return teamData, nil +} + +func CreateTeamWithData(s *xorm.Session, teamData TeamData, u *user.User) (team *models.Team, err error) { + tea := &models.Team{ + Name: teamData.TeamName, + Description: teamData.Description, + OidcID: teamData.OidcID, + } + log.Debugf("Teams: %v", tea.OidcID) + + err = tea.Create(s, u) + return tea, err +} + +// this functions creates an array of existing teams that was generated from the oidc data. +func GetOrCreateTeamsByNames(s *xorm.Session, teamData []TeamData, u *user.User) (te []models.Team, err error) { + te = []models.Team{} + // Procedure can only be successful if oidcID is set and converted to string + for _, oidcTeam := range teamData { + team, err := models.GetTeamByOidcIDAndName(s, oidcTeam.OidcID, oidcTeam.TeamName) + if err != nil { + log.Debugf("Team with oidc_id %v and name %v does not exist. Create Team.. ", oidcTeam.OidcID, oidcTeam.TeamName) + newTeam, err := CreateTeamWithData(s, oidcTeam, u) + if err != nil { + return te, err + } + te = append(te, *newTeam) + } else { + log.Debugf("Team with oidc_id %v and name %v already exists.", team.OidcID, team.Name) + te = append(te, team) + } + + } + log.Debugf("Array: %v", te) return te, err } From a2d5f06cde8ad5d68a4dc7cdcbe5ddaf00716190 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 13:49:30 +0100 Subject: [PATCH 11/38] do the swag --- pkg/swagger/docs.go | 13 +++++++++++++ pkg/swagger/swagger.json | 13 +++++++++++++ pkg/swagger/swagger.yaml | 10 ++++++++++ 3 files changed, 36 insertions(+) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 90b59f570..808e9d2e1 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -8740,6 +8740,11 @@ const docTemplate = `{ "maxLength": 250, "minLength": 1 }, + "oidc_id": { + "description": "The team's oidc id delivered by the oidc provider", + "type": "string", + "maxLength": 250 + }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" @@ -8901,6 +8906,11 @@ const docTemplate = `{ "maxLength": 250, "minLength": 1 }, + "oidc_id": { + "description": "The team's oidc id delivered by the oidc provider", + "type": "string", + "maxLength": 250 + }, "right": { "$ref": "#/definitions/models.Right" }, @@ -8997,6 +9007,9 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "scope": { + "type": "string" } } }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index bf9681b55..926da7d25 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -8731,6 +8731,11 @@ "maxLength": 250, "minLength": 1 }, + "oidc_id": { + "description": "The team's oidc id delivered by the oidc provider", + "type": "string", + "maxLength": 250 + }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" @@ -8892,6 +8897,11 @@ "maxLength": 250, "minLength": 1 }, + "oidc_id": { + "description": "The team's oidc id delivered by the oidc provider", + "type": "string", + "maxLength": 250 + }, "right": { "$ref": "#/definitions/models.Right" }, @@ -8988,6 +8998,9 @@ }, "name": { "type": "string" + }, + "scope": { + "type": "string" } } }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 393d2054b..102d6af3b 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -924,6 +924,10 @@ definitions: maxLength: 250 minLength: 1 type: string + oidc_id: + description: The team's oidc id delivered by the oidc provider + maxLength: 250 + type: string updated: description: A timestamp when this relation was last updated. You cannot change this value. @@ -1051,6 +1055,10 @@ definitions: maxLength: 250 minLength: 1 type: string + oidc_id: + description: The team's oidc id delivered by the oidc provider + maxLength: 250 + type: string right: $ref: '#/definitions/models.Right' updated: @@ -1124,6 +1132,8 @@ definitions: type: string name: type: string + scope: + type: string type: object todoist.Migration: properties: From af1e71f25667e34edbde6a2dd779fcebfe2c7bec Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 16:53:14 +0100 Subject: [PATCH 12/38] change method function to GetOrCreateTeamsByOIDCAndNames --- pkg/modules/auth/openid/openid.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 0335166d4..1780bedc6 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -215,7 +215,7 @@ func HandleCallback(c echo.Context) error { // find or create Teams and assign user as teammember. if len(teamData) > 0 { log.Debugf("TeamData is set %v", teamData) - teams, err := GetOrCreateTeamsByNames(s, teamData, u) + teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) if err != nil { log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) return err @@ -297,7 +297,7 @@ func CreateTeamWithData(s *xorm.Session, teamData TeamData, u *user.User) (team } // this functions creates an array of existing teams that was generated from the oidc data. -func GetOrCreateTeamsByNames(s *xorm.Session, teamData []TeamData, u *user.User) (te []models.Team, err error) { +func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []TeamData, u *user.User) (te []models.Team, err error) { te = []models.Team{} // Procedure can only be successful if oidcID is set and converted to string for _, oidcTeam := range teamData { From b272e46d4ddb7056654eca6abc71bcb625d64203 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 27 Jan 2023 17:15:50 +0100 Subject: [PATCH 13/38] add functionality for deleting user only from oidc teams which are not present in the current token --- pkg/models/error.go | 68 ++++++++++++++++++++----------- pkg/models/teams.go | 26 ++++++++++-- pkg/modules/auth/openid/openid.go | 54 +++++++++++++++++++++--- 3 files changed, 117 insertions(+), 31 deletions(-) diff --git a/pkg/models/error.go b/pkg/models/error.go index c63ee71eb..c0d21de92 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1072,7 +1072,6 @@ func (err ErrTeamNameCannotBeEmpty) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTeamNameCannotBeEmpty, Message: "The team name cannot be empty"} } -// ErrTeamDoesNotExist represents an error where a team does not exist type ErrTeamDoesNotExist struct { TeamID int64 } @@ -1095,28 +1094,6 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."} } -type ErrTeamsDoNotExist struct { - Name string -} - -// IsErrTeamDoNotExist checks if an error is ErrTeamDoesNotExist. -func IsErrTeamsDoNotExist(err error) bool { - _, ok := err.(ErrTeamsDoNotExist) - return ok -} - -func (err ErrTeamsDoNotExist) Error() string { - return fmt.Sprintf("Team does not exist [Team Name: %v]", err.Name) -} - -// ErrCodeTeamDoesNotExist holds the unique world-error code of this error -const ErrCodeTeamsDoNotExist = 6002 - -// HTTPError holds the http error description -func (err ErrTeamsDoNotExist) HTTPError() web.HTTPError { - return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No team with given name exists."} -} - // ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace type ErrTeamAlreadyHasAccess struct { TeamID int64 @@ -1213,6 +1190,51 @@ func (err ErrTeamDoesNotHaveAccessToList) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."} } +type ErrTeamsDoNotExist struct { + Name string +} + +// IsErrTeamDoNotExist checks if an error is ErrTeamDoesNotExist. +func IsErrTeamsDoNotExist(err error) bool { + _, ok := err.(ErrTeamsDoNotExist) + return ok +} + +func (err ErrTeamsDoNotExist) Error() string { + return fmt.Sprintf("Team does not exist [Team Name: %v]", err.Name) +} + +// ErrCodeTeamDoesNotExist holds the unique world-error code of this error +const ErrCodeTeamsDoNotExist = 6008 + +// HTTPError holds the http error description +func (err ErrTeamsDoNotExist) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No team with given name exists."} +} + +// ErrOIDCTeamsDoNotExistForUser represents an error where an oidcTeam does not exist for the user +type ErrOIDCTeamsDoNotExistForUser struct { + UserID int64 +} + +// IsErrOIDCTeamsDoNotExistForUser checks if an error is ErrOIDCTeamsDoNotExistForUser. +func IsErrOIDCTeamsDoNotExistForUser(err error) bool { + _, ok := err.(ErrTeamDoesNotExist) + return ok +} + +func (err ErrOIDCTeamsDoNotExistForUser) Error() string { + return fmt.Sprintf("No Oidc exists for User [User ID: %d]", err.UserID) +} + +// ErrCodeTeamDoesNotExist holds the unique world-error code of this error +const ErrCodeOIDCTeamsDoNotExistForUser = 6009 + +// HTTPError holds the http error description +func (err ErrOIDCTeamsDoNotExistForUser) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."} +} + // ==================== // User <-> List errors // ==================== diff --git a/pkg/models/teams.go b/pkg/models/teams.go index f0ecbf541..9becb12a6 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -94,6 +94,12 @@ type TeamUser struct { TeamID int64 `json:"-"` } +type TeamData struct { + TeamName string + OidcID string + Description string +} + // GetTeamByID gets a team by its ID func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) { if id < 1 { @@ -143,16 +149,30 @@ func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { // GetTeamByOidcIDAndName gets teams where oidc_id and name match parameters // For oidc team creation oidcID and Name need to be set -func GetTeamByOidcIDAndName(s *xorm.Session, id string, name string) (team Team, err error) { +func GetTeamByOidcIDAndName(s *xorm.Session, oidcID string, teamName string) (team Team, err error) { exists, err := s. Table("teams"). - Where("oidc_id = ? AND name = ?", id, name). + Where("oidc_id = ? AND name = ?", oidcID, teamName). Get(&team) log.Debugf("GetTeamByOidcIDAndName: %v, exists: %v", team.Name, exists) if exists && err == nil { return team, nil } - return team, ErrTeamsDoNotExist{id} + return team, ErrTeamsDoNotExist{oidcID} +} + +func FindAllOidcTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err error) { + err = s. + Table("team_members"). + Where("user_id = ? ", userID). + Join("RIGHT", "teams", "teams.id = team_members.team_id"). + Where("teams.oidc_id != ?", ""). + Cols("teams.id"). + Find(&ts) + if ts == nil || err != nil { + return ts, ErrOIDCTeamsDoNotExistForUser{userID} + } + return ts, nil } func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) { diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 1780bedc6..12481c910 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -211,16 +211,22 @@ func HandleCallback(c echo.Context) error { log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) return handler.HandleHTTPError(err, c) } - // check if we have seen these teams before. - // find or create Teams and assign user as teammember. + + //TODO: fix this error check + // nil is no problem + if len(teamData) > 0 { + //find old teams for user through oidc + oldOidcTeams, _ := models.FindAllOidcTeamIDsForUser(s, u.ID) + // check if we have seen these teams before. + // find or create Teams and assign user as teammember. + var oidcTeams []int64 log.Debugf("TeamData is set %v", teamData) teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) if err != nil { log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) return err } - for _, team := range teams { tm := models.TeamMember{TeamID: team.ID, Username: u.Username} exists, err := tm.CheckMembership(s) @@ -232,9 +238,10 @@ func HandleCallback(c echo.Context) error { } else { log.Debugf("Team exists? %v or error: %v", exists, err) } + oidcTeams = append(oidcTeams, team.ID) } + SignOutFromOrDeleteTeamsByID(s, u, notIn(oldOidcTeams, oidcTeams)) } - err = s.Commit() if err != nil { _ = s.Rollback() @@ -245,13 +252,30 @@ func HandleCallback(c echo.Context) error { return auth.NewUserAuthTokenResponse(u, c, false) } +func SignOutFromOrDeleteTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) { + for _, teamID := range teamIDs { + tm := models.TeamMember{TeamID: teamID, Username: u.Username} + err := tm.Delete(s, u) + if err != nil { + team, err := models.GetTeamByID(s, teamID) + if err != nil { + log.Errorf("Cannot find team with id: %v, err: %v", teamID, err) + } else { + err = team.Delete(s, u) + if err != nil { + log.Errorf("Cannot delete team %v", err) + } + } + } + } +} + func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []TeamData, err error) { teamData = []TeamData{} if groups != nil { el := groups.([]interface{}) for _, data := range el { team := data.(map[string]interface{}) - log.Debugf("%s", team) var name string var description string var oidcID string @@ -397,3 +421,23 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us return } + +// find the elements which appear in slice1,but not in slice2 +func notIn(slice1 []int64, slice2 []int64) []int64 { + var diff []int64 + + for _, s1 := range slice1 { + found := false + for _, s2 := range slice2 { + if s1 == s2 { + found = true + break + } + } + // String not found. We add it to return slice + if !found { + diff = append(diff, s1) + } + } + return diff +} From b32656c46f6907d067d05b140dae68837857aad8 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 1 Feb 2023 16:34:35 +0100 Subject: [PATCH 14/38] make provider scopes more robust add default openid profile email --- pkg/modules/auth/openid/providers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/modules/auth/openid/providers.go b/pkg/modules/auth/openid/providers.go index a96f822c8..b37991ce1 100644 --- a/pkg/modules/auth/openid/providers.go +++ b/pkg/modules/auth/openid/providers.go @@ -125,6 +125,10 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro logoutURL = "" } + scope, _ := pi["scope"].(string) + if scope == "" { + scope = "openid profile email" + } provider = &Provider{ Name: pi["name"].(string), Key: k, @@ -132,7 +136,7 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro OriginalAuthURL: pi["authurl"].(string), ClientSecret: pi["clientsecret"].(string), LogoutURL: logoutURL, - Scope: pi["scope"].(string), + Scope: scope, } cl, is := pi["clientid"].(int) From 2eb0bb9c14886b6acd6424c77cd0dfb1e5175be1 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 1 Feb 2023 16:35:12 +0100 Subject: [PATCH 15/38] remove user from all oidc teams if token is empty --- pkg/modules/auth/openid/openid.go | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 12481c910..7f57296a4 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -212,15 +212,12 @@ func HandleCallback(c echo.Context) error { return handler.HandleHTTPError(err, c) } - //TODO: fix this error check - // nil is no problem - + //find old teams for user through oidc + oldOidcTeams, _ := models.FindAllOidcTeamIDsForUser(s, u.ID) + var oidcTeams []int64 if len(teamData) > 0 { - //find old teams for user through oidc - oldOidcTeams, _ := models.FindAllOidcTeamIDsForUser(s, u.ID) // check if we have seen these teams before. // find or create Teams and assign user as teammember. - var oidcTeams []int64 log.Debugf("TeamData is set %v", teamData) teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) if err != nil { @@ -240,8 +237,8 @@ func HandleCallback(c echo.Context) error { } oidcTeams = append(oidcTeams, team.ID) } - SignOutFromOrDeleteTeamsByID(s, u, notIn(oldOidcTeams, oidcTeams)) } + SignOutFromOrDeleteTeamsByID(s, u, notIn(oldOidcTeams, oidcTeams)) err = s.Commit() if err != nil { _ = s.Rollback() @@ -343,12 +340,6 @@ func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []TeamData, u *use return te, err } -// assign user to team -// remove user from team if not in group -// if multiple teams found with same name -> do nothing -// optional: assign by id -// - func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *user.User, err error) { // Check if the user exists for that issuer and subject From 1b191c36e4716f8f68181ca5ea12ec49f834767c Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 1 Feb 2023 16:35:27 +0100 Subject: [PATCH 16/38] add config.yml.sample for seting up vikunja_scope and group assignment feature --- config.yml.sample | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config.yml.sample b/config.yml.sample index 6b3225b94..209a29d33 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -303,6 +303,10 @@ auth: clientid: # The client secret used to authenticate Vikunja at the OpenID Connect provider. clientsecret: + # The scope necessary to use oidc. + # If you want to use the Feature to create and assign to vikunja teams via oidc, you have to add the custom "vikunja_scope". + # e.g. scope: openid email profile vikunja_scope + scope: openid email profile # Prometheus metrics endpoint metrics: From 8d46490db0e76542a126e23f0c59de9900807c90 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 1 Feb 2023 16:36:01 +0100 Subject: [PATCH 17/38] add openid.md as readme for feature: 950 assigning group through oidc claim --- pkg/modules/auth/openid/openid.md | 99 +++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 pkg/modules/auth/openid/openid.md diff --git a/pkg/modules/auth/openid/openid.md b/pkg/modules/auth/openid/openid.md new file mode 100644 index 000000000..c3db48822 --- /dev/null +++ b/pkg/modules/auth/openid/openid.md @@ -0,0 +1,99 @@ +regarding: +https://kolaente.dev/vikunja/api/pulls/1279 + +# Assign teams via oidc +This PR adds the functionality to assign users to teams via oidc. +Read carefully and brief your administrators to use this feature. +Tested with oidc provider authentik. +To distinguish between groups created in vikunja and groups generated via oidc, there is an attribute neccessary, which is called: *oidcID* + +## Setup +Edit config.yml to include scope: openid profile email vikunja_scope + +For authentik to use group assignment feature: +- go to: .../if/admin/#/core/property-mappings + +- create a new mapping called "vikunja_scope" + +There is a field to enter python expressions that will be delivered with the oidc token. + +- write a small script, for adding group information to vikunja_scope. + + +```python +groupsDict = {"vikunja_groups": []} +for group in request.user.ak_groups.all(): + groupsDict["vikunja_groups"].append({"name": group.name, "oidcID": group.num_pk}) +return groupsDict + +""" +output example: +{ + "vikunja_groups": [ + { + "name": "team 1", + "oidcID": 33349 + }, + { + "name": "team 2", + "oidcID": 35933 + } + ] +} +""" +``` + +Now when you log in via oidc there will be a list of scopes you are claiming from your oidc provider. +You should see "the description you entered in the oidc provider's admin area" + +- Log in and go to teams. +- You will see "(sso: XXXXX)" written next to each team you were asigned through oidc. + + +## IMPORTANT NOTES: +**SSO/OIDC teams cannot be edited.** + +**It is crucial to call the element "vikunja_groups" since this is the name vikunja is looking for.** + +**Additionally, make sure to deliver an "oidcID" and a "name".** + + + + +____________________________________________________________________________ + +## BEHAVIOR + +*(.. examples for "team1" ..)* + +1. *Token delivers team.name +team.oidcId and Vikunja team does not exist:* \ +New team will be created called "team 1" with attribute oidcId: "33929" + + +2. *In Vikunja Team with name "team 1" already exists in vikunja, but has no oidcID set:* \ +new team will be created called "team 1" with attribute oidcId: "33929" + + +3. *In Vikunja Team with name "team 1" already exists in vikunja, but has different oidcID set:* \ +new team will be created called "team 1" with attribute oidcId: "33929" + + +4. *In Vikunja Team with oidcID "33929" already exists in vikunja, but has different name than "team1":* \ +new team will be created called "team 1" with attribute oidcId: "33929" + + +5. *Scope vikunja_scope is not set:* \ +nothing happens + + +6. *oidcID is not set:* \ +You'll get error. +Custom Scope malformed +"The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID." + +7. *In Vikunja I am in "team 3" with oidcID "", but the token does not deliver any data for "team 3":* \ +You will stay in team 3 since it was not set by the oidc provider + +8. *In Vikunja I am in "team 3" with oidcID "12345", but the token does not deliver any data for "team 3"*:\ +You will be signed out of all teams, which have an oidcID set and are not contained in the token. +Especially if you've been the last team member, the team will be deleted. \ No newline at end of file From 4567a6806556386ae8bda18be2668ab83f243ff8 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 10 Feb 2023 21:44:08 +0100 Subject: [PATCH 18/38] do the swag --- pkg/swagger/docs.go | 436 ++++++++++++++++----------------------- pkg/swagger/swagger.json | 436 ++++++++++++++++----------------------- pkg/swagger/swagger.yaml | 252 +++++++++++----------- 3 files changed, 476 insertions(+), 648 deletions(-) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 808e9d2e1..1274e01b0 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7620,11 +7620,36 @@ const docTemplate = `{ }, "created_by": { "description": "The user who initially created the bucket.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" + }, + "filter_by": { + "description": "The field name of the field to filter by", + "type": "array", + "items": { + "type": "string" + } + }, + "filter_comparator": { + "description": "The comparator for field and value", + "type": "array", + "items": { + "type": "string" + } + }, + "filter_concat": { + "description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,", + "type": "string" + }, + "filter_include_nulls": { + "description": "If set to true, the result will also include null values", + "type": "boolean" + }, + "filter_value": { + "description": "The value of the field name to filter by", + "type": "array", + "items": { + "type": "string" + } }, "id": { "description": "The unique, numeric id of this bucket.", @@ -7643,10 +7668,24 @@ const docTemplate = `{ "description": "The list this bucket belongs to.", "type": "integer" }, + "order_by": { + "description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.", + "type": "array", + "items": { + "type": "string" + } + }, "position": { "description": "The position this bucket has when querying all buckets. See the tasks.position property on how to use this.", "type": "number" }, + "sort_by": { + "description": "The query parameter to sort by. This is for ex. done, priority, etc.", + "type": "array", + "items": { + "type": "string" + } + }, "tasks": { "description": "All tasks which belong to this bucket.", "type": "array", @@ -7662,7 +7701,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this bucket was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.BulkAssignees": { @@ -7674,7 +7715,9 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/user.User" } - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.BulkTask": { @@ -7708,11 +7751,7 @@ const docTemplate = `{ }, "created_by": { "description": "The user who initially created the task.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The task description.", @@ -7784,11 +7823,7 @@ const docTemplate = `{ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "allOf": [ - { - "$ref": "#/definitions/models.RelatedTaskMap" - } - ] + "$ref": "#/definitions/models.RelatedTaskMap" }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -7803,11 +7838,7 @@ const docTemplate = `{ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "allOf": [ - { - "$ref": "#/definitions/models.TaskRepeatMode" - } - ] + "type": "integer" }, "start_date": { "description": "When this task starts.", @@ -7815,11 +7846,7 @@ const docTemplate = `{ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "task_ids": { "description": "A list of task ids to update", @@ -7836,7 +7863,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.DatabaseNotifications": { @@ -7864,7 +7893,9 @@ const docTemplate = `{ "read_at": { "description": "When this notification is marked as read, this will be updated with the current timestamp.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.Label": { @@ -7876,11 +7907,7 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this label", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The label description.", @@ -7904,7 +7931,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this label was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.LabelTask": { @@ -7917,7 +7946,9 @@ const docTemplate = `{ "label_id": { "description": "The label id you want to associate with a task.", "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.LabelTaskBulk": { @@ -7929,7 +7960,9 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/models.Label" } - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.LinkSharing": { @@ -7957,36 +7990,26 @@ const docTemplate = `{ }, "right": { "description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "shared_by": { "description": "The user who shared this list", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "sharing_type": { "description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.SharingType" - } - ] + "maximum": 2 }, "updated": { "description": "A timestamp when this share was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.List": { @@ -8035,11 +8058,7 @@ const docTemplate = `{ }, "owner": { "description": "The user who created this list.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "position": { "description": "The position this list has when querying all lists. See the tasks.position property on how to use this.", @@ -8047,11 +8066,7 @@ const docTemplate = `{ }, "subscription": { "description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The title of the list. You'll see this in the namespace overview.", @@ -8062,7 +8077,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this list was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.ListDuplicate": { @@ -8070,16 +8087,14 @@ const docTemplate = `{ "properties": { "list": { "description": "The copied list", - "allOf": [ - { - "$ref": "#/definitions/models.List" - } - ] + "$ref": "#/definitions/models.List" }, "namespace_id": { "description": "The target namespace ID", "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.ListUser": { @@ -8095,13 +8110,9 @@ const docTemplate = `{ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8110,7 +8121,9 @@ const docTemplate = `{ "user_id": { "description": "The username.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.Message": { @@ -8148,19 +8161,11 @@ const docTemplate = `{ }, "owner": { "description": "The user who owns this namespace", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The name of this namespace.", @@ -8171,7 +8176,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.NamespaceUser": { @@ -8187,13 +8194,9 @@ const docTemplate = `{ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8202,7 +8205,9 @@ const docTemplate = `{ "user_id": { "description": "The username.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.NamespaceWithLists": { @@ -8237,19 +8242,11 @@ const docTemplate = `{ }, "owner": { "description": "The user who owns this namespace", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The name of this namespace.", @@ -8260,7 +8257,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.RelatedTaskMap": { @@ -8272,50 +8271,6 @@ const docTemplate = `{ } } }, - "models.RelationKind": { - "type": "string", - "enum": [ - "unknown", - "subtask", - "parenttask", - "related", - "duplicateof", - "duplicates", - "blocking", - "blocked", - "precedes", - "follows", - "copiedfrom", - "copiedto" - ], - "x-enum-varnames": [ - "RelationKindUnknown", - "RelationKindSubtask", - "RelationKindParenttask", - "RelationKindRelated", - "RelationKindDuplicateOf", - "RelationKindDuplicates", - "RelationKindBlocking", - "RelationKindBlocked", - "RelationKindPreceeds", - "RelationKindFollows", - "RelationKindCopiedFrom", - "RelationKindCopiedTo" - ] - }, - "models.Right": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "RightRead", - "RightWrite", - "RightAdmin" - ] - }, "models.SavedFilter": { "type": "object", "properties": { @@ -8329,11 +8284,7 @@ const docTemplate = `{ }, "filters": { "description": "The actual filters this filter contains", - "allOf": [ - { - "$ref": "#/definitions/models.TaskCollection" - } - ] + "$ref": "#/definitions/models.TaskCollection" }, "id": { "description": "The unique numeric id of this saved filter", @@ -8345,11 +8296,7 @@ const docTemplate = `{ }, "owner": { "description": "The user who owns this filter", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "title": { "description": "The title of the filter.", @@ -8360,22 +8307,11 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this filter was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, - "models.SharingType": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "SharingTypeUnknown", - "SharingTypeWithoutPassword", - "SharingTypeWithPassword" - ] - }, "models.Subscription": { "type": "object", "properties": { @@ -8396,12 +8332,10 @@ const docTemplate = `{ }, "user": { "description": "The user who made this subscription", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] - } + "$ref": "#/definitions/user.User" + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.Task": { @@ -8435,11 +8369,7 @@ const docTemplate = `{ }, "created_by": { "description": "The user who initially created the task.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The task description.", @@ -8511,11 +8441,7 @@ const docTemplate = `{ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "allOf": [ - { - "$ref": "#/definitions/models.RelatedTaskMap" - } - ] + "$ref": "#/definitions/models.RelatedTaskMap" }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -8530,11 +8456,7 @@ const docTemplate = `{ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "allOf": [ - { - "$ref": "#/definitions/models.TaskRepeatMode" - } - ] + "type": "integer" }, "start_date": { "description": "When this task starts.", @@ -8542,11 +8464,7 @@ const docTemplate = `{ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The task text. This is what you'll see in the list.", @@ -8556,7 +8474,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskAssginee": { @@ -8567,7 +8487,9 @@ const docTemplate = `{ }, "user_id": { "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskAttachment": { @@ -8587,7 +8509,9 @@ const docTemplate = `{ }, "task_id": { "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskCollection": { @@ -8635,7 +8559,9 @@ const docTemplate = `{ "items": { "type": "string" } - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskComment": { @@ -8655,7 +8581,9 @@ const docTemplate = `{ }, "updated": { "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskRelation": { @@ -8667,11 +8595,7 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this relation", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "other_task_id": { "description": "The ID of the other task, the task which is being related.", @@ -8679,31 +8603,16 @@ const docTemplate = `{ }, "relation_kind": { "description": "The kind of the relation.", - "allOf": [ - { - "$ref": "#/definitions/models.RelationKind" - } - ] + "type": "string" }, "task_id": { "description": "The ID of the \"base\" task, the task which has a relation to another.", "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, - "models.TaskRepeatMode": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "TaskRepeatModeDefault", - "TaskRepeatModeMonth", - "TaskRepeatModeFromCurrentDate" - ] - }, "models.Team": { "type": "object", "properties": { @@ -8713,11 +8622,7 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this team.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The team's description.", @@ -8748,7 +8653,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamList": { @@ -8764,13 +8671,9 @@ const docTemplate = `{ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "team_id": { "description": "The team id.", @@ -8779,7 +8682,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamMember": { @@ -8800,7 +8705,9 @@ const docTemplate = `{ "username": { "description": "The username of the member. We use this to prevent automated user id entering.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamNamespace": { @@ -8816,13 +8723,9 @@ const docTemplate = `{ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "team_id": { "description": "The team id.", @@ -8831,7 +8734,9 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamUser": { @@ -8867,7 +8772,8 @@ const docTemplate = `{ "type": "string", "maxLength": 250, "minLength": 1 - } + }, + "web.Auth": {} } }, "models.TeamWithRight": { @@ -8879,11 +8785,7 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this team.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The team's description.", @@ -8912,12 +8814,16 @@ const docTemplate = `{ "maxLength": 250 }, "right": { - "$ref": "#/definitions/models.Right" + "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", + "default": 0 }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.UserWithRight": { @@ -8941,7 +8847,9 @@ const docTemplate = `{ "type": "string" }, "right": { - "$ref": "#/definitions/models.Right" + "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", + "default": 0 }, "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", @@ -8952,7 +8860,8 @@ const docTemplate = `{ "type": "string", "maxLength": 250, "minLength": 1 - } + }, + "web.Auth": {} } }, "notifications.DatabaseNotification": { @@ -9187,7 +9096,8 @@ const docTemplate = `{ "type": "string", "maxLength": 250, "minLength": 1 - } + }, + "web.Auth": {} } }, "v1.LinkShareAuth": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 926da7d25..f2fe657a0 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7611,11 +7611,36 @@ }, "created_by": { "description": "The user who initially created the bucket.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" + }, + "filter_by": { + "description": "The field name of the field to filter by", + "type": "array", + "items": { + "type": "string" + } + }, + "filter_comparator": { + "description": "The comparator for field and value", + "type": "array", + "items": { + "type": "string" + } + }, + "filter_concat": { + "description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,", + "type": "string" + }, + "filter_include_nulls": { + "description": "If set to true, the result will also include null values", + "type": "boolean" + }, + "filter_value": { + "description": "The value of the field name to filter by", + "type": "array", + "items": { + "type": "string" + } }, "id": { "description": "The unique, numeric id of this bucket.", @@ -7634,10 +7659,24 @@ "description": "The list this bucket belongs to.", "type": "integer" }, + "order_by": { + "description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.", + "type": "array", + "items": { + "type": "string" + } + }, "position": { "description": "The position this bucket has when querying all buckets. See the tasks.position property on how to use this.", "type": "number" }, + "sort_by": { + "description": "The query parameter to sort by. This is for ex. done, priority, etc.", + "type": "array", + "items": { + "type": "string" + } + }, "tasks": { "description": "All tasks which belong to this bucket.", "type": "array", @@ -7653,7 +7692,9 @@ "updated": { "description": "A timestamp when this bucket was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.BulkAssignees": { @@ -7665,7 +7706,9 @@ "items": { "$ref": "#/definitions/user.User" } - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.BulkTask": { @@ -7699,11 +7742,7 @@ }, "created_by": { "description": "The user who initially created the task.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The task description.", @@ -7775,11 +7814,7 @@ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "allOf": [ - { - "$ref": "#/definitions/models.RelatedTaskMap" - } - ] + "$ref": "#/definitions/models.RelatedTaskMap" }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -7794,11 +7829,7 @@ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "allOf": [ - { - "$ref": "#/definitions/models.TaskRepeatMode" - } - ] + "type": "integer" }, "start_date": { "description": "When this task starts.", @@ -7806,11 +7837,7 @@ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "task_ids": { "description": "A list of task ids to update", @@ -7827,7 +7854,9 @@ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.DatabaseNotifications": { @@ -7855,7 +7884,9 @@ "read_at": { "description": "When this notification is marked as read, this will be updated with the current timestamp.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.Label": { @@ -7867,11 +7898,7 @@ }, "created_by": { "description": "The user who created this label", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The label description.", @@ -7895,7 +7922,9 @@ "updated": { "description": "A timestamp when this label was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.LabelTask": { @@ -7908,7 +7937,9 @@ "label_id": { "description": "The label id you want to associate with a task.", "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.LabelTaskBulk": { @@ -7920,7 +7951,9 @@ "items": { "$ref": "#/definitions/models.Label" } - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.LinkSharing": { @@ -7948,36 +7981,26 @@ }, "right": { "description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "shared_by": { "description": "The user who shared this list", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "sharing_type": { "description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.SharingType" - } - ] + "maximum": 2 }, "updated": { "description": "A timestamp when this share was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.List": { @@ -8026,11 +8049,7 @@ }, "owner": { "description": "The user who created this list.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "position": { "description": "The position this list has when querying all lists. See the tasks.position property on how to use this.", @@ -8038,11 +8057,7 @@ }, "subscription": { "description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The title of the list. You'll see this in the namespace overview.", @@ -8053,7 +8068,9 @@ "updated": { "description": "A timestamp when this list was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.ListDuplicate": { @@ -8061,16 +8078,14 @@ "properties": { "list": { "description": "The copied list", - "allOf": [ - { - "$ref": "#/definitions/models.List" - } - ] + "$ref": "#/definitions/models.List" }, "namespace_id": { "description": "The target namespace ID", "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.ListUser": { @@ -8086,13 +8101,9 @@ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8101,7 +8112,9 @@ "user_id": { "description": "The username.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.Message": { @@ -8139,19 +8152,11 @@ }, "owner": { "description": "The user who owns this namespace", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The name of this namespace.", @@ -8162,7 +8167,9 @@ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.NamespaceUser": { @@ -8178,13 +8185,9 @@ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8193,7 +8196,9 @@ "user_id": { "description": "The username.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.NamespaceWithLists": { @@ -8228,19 +8233,11 @@ }, "owner": { "description": "The user who owns this namespace", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The name of this namespace.", @@ -8251,7 +8248,9 @@ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.RelatedTaskMap": { @@ -8263,50 +8262,6 @@ } } }, - "models.RelationKind": { - "type": "string", - "enum": [ - "unknown", - "subtask", - "parenttask", - "related", - "duplicateof", - "duplicates", - "blocking", - "blocked", - "precedes", - "follows", - "copiedfrom", - "copiedto" - ], - "x-enum-varnames": [ - "RelationKindUnknown", - "RelationKindSubtask", - "RelationKindParenttask", - "RelationKindRelated", - "RelationKindDuplicateOf", - "RelationKindDuplicates", - "RelationKindBlocking", - "RelationKindBlocked", - "RelationKindPreceeds", - "RelationKindFollows", - "RelationKindCopiedFrom", - "RelationKindCopiedTo" - ] - }, - "models.Right": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "RightRead", - "RightWrite", - "RightAdmin" - ] - }, "models.SavedFilter": { "type": "object", "properties": { @@ -8320,11 +8275,7 @@ }, "filters": { "description": "The actual filters this filter contains", - "allOf": [ - { - "$ref": "#/definitions/models.TaskCollection" - } - ] + "$ref": "#/definitions/models.TaskCollection" }, "id": { "description": "The unique numeric id of this saved filter", @@ -8336,11 +8287,7 @@ }, "owner": { "description": "The user who owns this filter", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "title": { "description": "The title of the filter.", @@ -8351,22 +8298,11 @@ "updated": { "description": "A timestamp when this filter was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, - "models.SharingType": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "SharingTypeUnknown", - "SharingTypeWithoutPassword", - "SharingTypeWithPassword" - ] - }, "models.Subscription": { "type": "object", "properties": { @@ -8387,12 +8323,10 @@ }, "user": { "description": "The user who made this subscription", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] - } + "$ref": "#/definitions/user.User" + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.Task": { @@ -8426,11 +8360,7 @@ }, "created_by": { "description": "The user who initially created the task.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The task description.", @@ -8502,11 +8432,7 @@ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "allOf": [ - { - "$ref": "#/definitions/models.RelatedTaskMap" - } - ] + "$ref": "#/definitions/models.RelatedTaskMap" }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -8521,11 +8447,7 @@ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "allOf": [ - { - "$ref": "#/definitions/models.TaskRepeatMode" - } - ] + "type": "integer" }, "start_date": { "description": "When this task starts.", @@ -8533,11 +8455,7 @@ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "allOf": [ - { - "$ref": "#/definitions/models.Subscription" - } - ] + "$ref": "#/definitions/models.Subscription" }, "title": { "description": "The task text. This is what you'll see in the list.", @@ -8547,7 +8465,9 @@ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskAssginee": { @@ -8558,7 +8478,9 @@ }, "user_id": { "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskAttachment": { @@ -8578,7 +8500,9 @@ }, "task_id": { "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskCollection": { @@ -8626,7 +8550,9 @@ "items": { "type": "string" } - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskComment": { @@ -8646,7 +8572,9 @@ }, "updated": { "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TaskRelation": { @@ -8658,11 +8586,7 @@ }, "created_by": { "description": "The user who created this relation", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "other_task_id": { "description": "The ID of the other task, the task which is being related.", @@ -8670,31 +8594,16 @@ }, "relation_kind": { "description": "The kind of the relation.", - "allOf": [ - { - "$ref": "#/definitions/models.RelationKind" - } - ] + "type": "string" }, "task_id": { "description": "The ID of the \"base\" task, the task which has a relation to another.", "type": "integer" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, - "models.TaskRepeatMode": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "TaskRepeatModeDefault", - "TaskRepeatModeMonth", - "TaskRepeatModeFromCurrentDate" - ] - }, "models.Team": { "type": "object", "properties": { @@ -8704,11 +8613,7 @@ }, "created_by": { "description": "The user who created this team.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The team's description.", @@ -8739,7 +8644,9 @@ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamList": { @@ -8755,13 +8662,9 @@ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "team_id": { "description": "The team id.", @@ -8770,7 +8673,9 @@ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamMember": { @@ -8791,7 +8696,9 @@ "username": { "description": "The username of the member. We use this to prevent automated user id entering.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamNamespace": { @@ -8807,13 +8714,9 @@ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", "default": 0, - "maximum": 2, - "allOf": [ - { - "$ref": "#/definitions/models.Right" - } - ] + "maximum": 2 }, "team_id": { "description": "The team id.", @@ -8822,7 +8725,9 @@ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.TeamUser": { @@ -8858,7 +8763,8 @@ "type": "string", "maxLength": 250, "minLength": 1 - } + }, + "web.Auth": {} } }, "models.TeamWithRight": { @@ -8870,11 +8776,7 @@ }, "created_by": { "description": "The user who created this team.", - "allOf": [ - { - "$ref": "#/definitions/user.User" - } - ] + "$ref": "#/definitions/user.User" }, "description": { "description": "The team's description.", @@ -8903,12 +8805,16 @@ "maxLength": 250 }, "right": { - "$ref": "#/definitions/models.Right" + "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", + "default": 0 }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - } + }, + "web.CRUDable": {}, + "web.Rights": {} } }, "models.UserWithRight": { @@ -8932,7 +8838,9 @@ "type": "string" }, "right": { - "$ref": "#/definitions/models.Right" + "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", + "type": "integer", + "default": 0 }, "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", @@ -8943,7 +8851,8 @@ "type": "string", "maxLength": 250, "minLength": 1 - } + }, + "web.Auth": {} } }, "notifications.DatabaseNotification": { @@ -9178,7 +9087,8 @@ "type": "string", "maxLength": 250, "minLength": 1 - } + }, + "web.Auth": {} } }, "v1.LinkShareAuth": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 102d6af3b..31fa4f736 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -58,9 +58,30 @@ definitions: value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who initially created the bucket. + filter_by: + description: The field name of the field to filter by + items: + type: string + type: array + filter_comparator: + description: The comparator for field and value + items: + type: string + type: array + filter_concat: + description: The way all filter conditions are concatenated together, can + be either "and" or "or"., + type: string + filter_include_nulls: + description: If set to true, the result will also include null values + type: boolean + filter_value: + description: The value of the field name to filter by + items: + type: string + type: array id: description: The unique, numeric id of this bucket. type: integer @@ -76,10 +97,22 @@ definitions: list_id: description: The list this bucket belongs to. type: integer + order_by: + description: The query parameter to order the items by. This can be either + asc or desc, with asc being the default. + items: + type: string + type: array position: description: The position this bucket has when querying all buckets. See the tasks.position property on how to use this. type: number + sort_by: + description: The query parameter to sort by. This is for ex. done, priority, + etc. + items: + type: string + type: array tasks: description: All tasks which belong to this bucket. items: @@ -93,6 +126,8 @@ definitions: description: A timestamp when this bucket was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.BulkAssignees: properties: @@ -101,6 +136,8 @@ definitions: items: $ref: '#/definitions/user.User' type: array + web.CRUDable: {} + web.Rights: {} type: object models.BulkTask: properties: @@ -126,8 +163,7 @@ definitions: value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who initially created the task. description: description: The task description. @@ -192,8 +228,7 @@ definitions: sort by this later. type: integer related_tasks: - allOf: - - $ref: '#/definitions/models.RelatedTaskMap' + $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: description: An array of datetimes when the user wants to be reminded of the @@ -207,18 +242,16 @@ definitions: increase all remindes and the due date by its amount. type: integer repeat_mode: - allOf: - - $ref: '#/definitions/models.TaskRepeatMode' description: 'Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.' + type: integer start_date: description: When this task starts. type: string subscription: - allOf: - - $ref: '#/definitions/models.Subscription' + $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one task. @@ -235,6 +268,8 @@ definitions: description: A timestamp when this task was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.DatabaseNotifications: properties: @@ -259,6 +294,8 @@ definitions: description: When this notification is marked as read, this will be updated with the current timestamp. type: string + web.CRUDable: {} + web.Rights: {} type: object models.Label: properties: @@ -267,8 +304,7 @@ definitions: value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who created this label description: description: The label description. @@ -290,6 +326,8 @@ definitions: description: A timestamp when this label was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.LabelTask: properties: @@ -300,6 +338,8 @@ definitions: label_id: description: The label id you want to associate with a task. type: integer + web.CRUDable: {} + web.Rights: {} type: object models.LabelTaskBulk: properties: @@ -308,6 +348,8 @@ definitions: items: $ref: '#/definitions/models.Label' type: array + web.CRUDable: {} + web.Rights: {} type: object models.LinkSharing: properties: @@ -330,27 +372,26 @@ definitions: it after the link share has been created. type: string right: - allOf: - - $ref: '#/definitions/models.Right' default: 0 description: The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 + type: integer shared_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who shared this list sharing_type: - allOf: - - $ref: '#/definitions/models.SharingType' default: 0 description: The kind of this link. 0 = undefined, 1 = without password, 2 = with password. maximum: 2 + type: integer updated: description: A timestamp when this share was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.List: properties: @@ -392,16 +433,14 @@ definitions: namespace_id: type: integer owner: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who created this list. position: description: The position this list has when querying all lists. See the tasks.position property on how to use this. type: number subscription: - allOf: - - $ref: '#/definitions/models.Subscription' + $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one list. @@ -414,16 +453,19 @@ definitions: description: A timestamp when this list was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.ListDuplicate: properties: list: - allOf: - - $ref: '#/definitions/models.List' + $ref: '#/definitions/models.List' description: The copied list namespace_id: description: The target namespace ID type: integer + web.CRUDable: {} + web.Rights: {} type: object models.ListUser: properties: @@ -435,12 +477,11 @@ definitions: description: The unique, numeric id of this list <-> user relation. type: integer right: - allOf: - - $ref: '#/definitions/models.Right' default: 0 description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 + type: integer updated: description: A timestamp when this relation was last updated. You cannot change this value. @@ -448,6 +489,8 @@ definitions: user_id: description: The username. type: string + web.CRUDable: {} + web.Rights: {} type: object models.Message: properties: @@ -475,12 +518,10 @@ definitions: description: Whether or not a namespace is archived. type: boolean owner: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who owns this namespace subscription: - allOf: - - $ref: '#/definitions/models.Subscription' + $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one namespace. @@ -493,6 +534,8 @@ definitions: description: A timestamp when this namespace was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.NamespaceUser: properties: @@ -504,12 +547,11 @@ definitions: description: The unique, numeric id of this namespace <-> user relation. type: integer right: - allOf: - - $ref: '#/definitions/models.Right' default: 0 description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 + type: integer updated: description: A timestamp when this relation was last updated. You cannot change this value. @@ -517,6 +559,8 @@ definitions: user_id: description: The username. type: string + web.CRUDable: {} + web.Rights: {} type: object models.NamespaceWithLists: properties: @@ -542,12 +586,10 @@ definitions: $ref: '#/definitions/models.List' type: array owner: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who owns this namespace subscription: - allOf: - - $ref: '#/definitions/models.Subscription' + $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one namespace. @@ -560,6 +602,8 @@ definitions: description: A timestamp when this namespace was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.RelatedTaskMap: additionalProperties: @@ -567,44 +611,6 @@ definitions: $ref: '#/definitions/models.Task' type: array type: object - models.RelationKind: - enum: - - unknown - - subtask - - parenttask - - related - - duplicateof - - duplicates - - blocking - - blocked - - precedes - - follows - - copiedfrom - - copiedto - type: string - x-enum-varnames: - - RelationKindUnknown - - RelationKindSubtask - - RelationKindParenttask - - RelationKindRelated - - RelationKindDuplicateOf - - RelationKindDuplicates - - RelationKindBlocking - - RelationKindBlocked - - RelationKindPreceeds - - RelationKindFollows - - RelationKindCopiedFrom - - RelationKindCopiedTo - models.Right: - enum: - - 0 - - 1 - - 2 - type: integer - x-enum-varnames: - - RightRead - - RightWrite - - RightAdmin models.SavedFilter: properties: created: @@ -615,8 +621,7 @@ definitions: description: The description of the filter type: string filters: - allOf: - - $ref: '#/definitions/models.TaskCollection' + $ref: '#/definitions/models.TaskCollection' description: The actual filters this filter contains id: description: The unique numeric id of this saved filter @@ -626,8 +631,7 @@ definitions: a separate namespace together with favorite lists. type: boolean owner: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who owns this filter title: description: The title of the filter. @@ -638,17 +642,9 @@ definitions: description: A timestamp when this filter was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object - models.SharingType: - enum: - - 0 - - 1 - - 2 - type: integer - x-enum-varnames: - - SharingTypeUnknown - - SharingTypeWithoutPassword - - SharingTypeWithPassword models.Subscription: properties: created: @@ -664,9 +660,10 @@ definitions: description: The numeric ID of the subscription type: integer user: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who made this subscription + web.CRUDable: {} + web.Rights: {} type: object models.Task: properties: @@ -692,8 +689,7 @@ definitions: value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who initially created the task. description: description: The task description. @@ -758,8 +754,7 @@ definitions: sort by this later. type: integer related_tasks: - allOf: - - $ref: '#/definitions/models.RelatedTaskMap' + $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: description: An array of datetimes when the user wants to be reminded of the @@ -773,18 +768,16 @@ definitions: increase all remindes and the due date by its amount. type: integer repeat_mode: - allOf: - - $ref: '#/definitions/models.TaskRepeatMode' description: 'Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.' + type: integer start_date: description: When this task starts. type: string subscription: - allOf: - - $ref: '#/definitions/models.Subscription' + $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one task. @@ -796,6 +789,8 @@ definitions: description: A timestamp when this task was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.TaskAssginee: properties: @@ -803,6 +798,8 @@ definitions: type: string user_id: type: integer + web.CRUDable: {} + web.Rights: {} type: object models.TaskAttachment: properties: @@ -816,6 +813,8 @@ definitions: type: integer task_id: type: integer + web.CRUDable: {} + web.Rights: {} type: object models.TaskCollection: properties: @@ -853,6 +852,8 @@ definitions: items: type: string type: array + web.CRUDable: {} + web.Rights: {} type: object models.TaskComment: properties: @@ -866,6 +867,8 @@ definitions: type: integer updated: type: string + web.CRUDable: {} + web.Rights: {} type: object models.TaskRelation: properties: @@ -874,30 +877,20 @@ definitions: value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who created this relation other_task_id: description: The ID of the other task, the task which is being related. type: integer relation_kind: - allOf: - - $ref: '#/definitions/models.RelationKind' description: The kind of the relation. + type: string task_id: description: The ID of the "base" task, the task which has a relation to another. type: integer + web.CRUDable: {} + web.Rights: {} type: object - models.TaskRepeatMode: - enum: - - 0 - - 1 - - 2 - type: integer - x-enum-varnames: - - TaskRepeatModeDefault - - TaskRepeatModeMonth - - TaskRepeatModeFromCurrentDate models.Team: properties: created: @@ -905,8 +898,7 @@ definitions: this value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who created this team. description: description: The team's description. @@ -932,6 +924,8 @@ definitions: description: A timestamp when this relation was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.TeamList: properties: @@ -943,12 +937,11 @@ definitions: description: The unique, numeric id of this list <-> team relation. type: integer right: - allOf: - - $ref: '#/definitions/models.Right' default: 0 description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 + type: integer team_id: description: The team id. type: integer @@ -956,6 +949,8 @@ definitions: description: A timestamp when this relation was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.TeamMember: properties: @@ -974,6 +969,8 @@ definitions: description: The username of the member. We use this to prevent automated user id entering. type: string + web.CRUDable: {} + web.Rights: {} type: object models.TeamNamespace: properties: @@ -985,12 +982,11 @@ definitions: description: The unique, numeric id of this namespace <-> team relation. type: integer right: - allOf: - - $ref: '#/definitions/models.Right' default: 0 description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 + type: integer team_id: description: The team id. type: integer @@ -998,6 +994,8 @@ definitions: description: A timestamp when this relation was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.TeamUser: properties: @@ -1028,6 +1026,7 @@ definitions: maxLength: 250 minLength: 1 type: string + web.Auth: {} type: object models.TeamWithRight: properties: @@ -1036,8 +1035,7 @@ definitions: this value. type: string created_by: - allOf: - - $ref: '#/definitions/user.User' + $ref: '#/definitions/user.User' description: The user who created this team. description: description: The team's description. @@ -1060,11 +1058,16 @@ definitions: maxLength: 250 type: string right: - $ref: '#/definitions/models.Right' + default: 0 + description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = + Admin. See the docs for more details. + type: integer updated: description: A timestamp when this relation was last updated. You cannot change this value. type: string + web.CRUDable: {} + web.Rights: {} type: object models.UserWithRight: properties: @@ -1083,7 +1086,10 @@ definitions: description: The full name of the user. type: string right: - $ref: '#/definitions/models.Right' + default: 0 + description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = + Admin. See the docs for more details. + type: integer updated: description: A timestamp when this task was last updated. You cannot change this value. @@ -1093,6 +1099,7 @@ definitions: maxLength: 250 minLength: 1 type: string + web.Auth: {} type: object notifications.DatabaseNotification: properties: @@ -1263,6 +1270,7 @@ definitions: maxLength: 250 minLength: 1 type: string + web.Auth: {} type: object v1.LinkShareAuth: properties: From 3b4c5e4815d3cbb3a37eb752e6fd3a63075c7df3 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 16:14:03 +0100 Subject: [PATCH 19/38] add errors to error doc, rewrite error messages specify error on teams model, add more declarative error specify error message on ErrOIDCTeamDoesNotExist --- docs/content/doc/usage/errors.md | 5 ++++ pkg/models/error.go | 40 +++++++++++++++++++++++++------- pkg/models/teams.go | 2 +- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index faf4ef548..9dabb288c 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -111,6 +111,11 @@ This document describes the different errors Vikunja can return. | 6005 | 409 | The user is already a member of that team. | | 6006 | 400 | Cannot delete the last team member. | | 6007 | 403 | The team does not have access to the list to perform that action. | +| 6008 | 400 | There are no teams found with that team name | +| 6009 | 400 | There is no oidc team with that team name and oidcId | +| 6010 | 400 | There are no oidc teams found for the user | + + ## User List Access diff --git a/pkg/models/error.go b/pkg/models/error.go index c0d21de92..c343ad38c 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1194,22 +1194,46 @@ type ErrTeamsDoNotExist struct { Name string } -// IsErrTeamDoNotExist checks if an error is ErrTeamDoesNotExist. +// IsErrTeamsDoNotExist checks if an error is ErrTeamsDoNotExist. func IsErrTeamsDoNotExist(err error) bool { _, ok := err.(ErrTeamsDoNotExist) return ok } func (err ErrTeamsDoNotExist) Error() string { - return fmt.Sprintf("Team does not exist [Team Name: %v]", err.Name) + return fmt.Sprintf("No teams with that name exist [Team Name: %v]", err.Name) } -// ErrCodeTeamDoesNotExist holds the unique world-error code of this error +// ErrCodeTeamsDoesNotExist holds the unique world-error code of this error const ErrCodeTeamsDoNotExist = 6008 // HTTPError holds the http error description func (err ErrTeamsDoNotExist) HTTPError() web.HTTPError { - return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No team with given name exists."} + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No teams with that name exist"} +} + +// ErrOIDCTeamDoesNotExist represents an error where a team with specified name and specified oidcId property does not exist +type ErrOIDCTeamDoesNotExist struct { + OidcId string + Name string +} + +// IsErrOIDCTeamDoesNotExist checks if an error is ErrOIDCTeamDoesNotExist. +func IsErrOIDCTeamDoesNotExist(err error) bool { + _, ok := err.(ErrOIDCTeamDoesNotExist) + return ok +} + +func (err ErrOIDCTeamDoesNotExist) Error() string { + return fmt.Sprintf("No Team with that name and valid property oidcId could be found [Team Name: %v] [OidcId : %v] ", err.Name, err.OidcId) +} + +// ErrCodeTeamDoesNotExist holds the unique world-error code of this error +const ErrCodeOIDCTeamDoesNotExist = 6009 + +// HTTPError holds the http error description +func (err ErrOIDCTeamDoesNotExist) HTTPError() web.HTTPError { + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No Team with that name and valid property oidcId could be found."} } // ErrOIDCTeamsDoNotExistForUser represents an error where an oidcTeam does not exist for the user @@ -1219,20 +1243,20 @@ type ErrOIDCTeamsDoNotExistForUser struct { // IsErrOIDCTeamsDoNotExistForUser checks if an error is ErrOIDCTeamsDoNotExistForUser. func IsErrOIDCTeamsDoNotExistForUser(err error) bool { - _, ok := err.(ErrTeamDoesNotExist) + _, ok := err.(ErrOIDCTeamsDoNotExistForUser) return ok } func (err ErrOIDCTeamsDoNotExistForUser) Error() string { - return fmt.Sprintf("No Oidc exists for User [User ID: %d]", err.UserID) + return fmt.Sprintf("No Teams with property oidcId could be found for User [User ID: %d]", err.UserID) } // ErrCodeTeamDoesNotExist holds the unique world-error code of this error -const ErrCodeOIDCTeamsDoNotExistForUser = 6009 +const ErrCodeOIDCTeamsDoNotExistForUser = 6010 // HTTPError holds the http error description func (err ErrOIDCTeamsDoNotExistForUser) HTTPError() web.HTTPError { - return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."} + return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No Teams with property oidcId could be found for User."} } // ==================== diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 9becb12a6..37917d4e2 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -158,7 +158,7 @@ func GetTeamByOidcIDAndName(s *xorm.Session, oidcID string, teamName string) (te if exists && err == nil { return team, nil } - return team, ErrTeamsDoNotExist{oidcID} + return team, ErrOIDCTeamDoesNotExist{teamName, oidcID} } func FindAllOidcTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err error) { From e0a1eae2680238bc2a590947a8775beda73eef1d Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 17:05:34 +0100 Subject: [PATCH 20/38] rewok checkMembership to not load user and improvements on return val --- pkg/models/team_members.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index 39e12c33b..ced46aeed 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -110,18 +110,13 @@ func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) { } func (tm *TeamMember) CheckMembership(s *xorm.Session) (exists bool, err error) { - member, err := user2.GetUserByUsername(s, tm.Username) - if err != nil { - return - } - tm.UserID = member.ID exists, err = s. Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). Get(&TeamMember{}) - if exists { + if exists && err == nil { return } - return exists, ErrUserIsMemberOfTeam{tm.UserID, tm.UserID} + return exists, err } // Update toggles a team member's admin status From 37ccc008ee35f63236f3e832a058bb1507840733 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 17:24:42 +0100 Subject: [PATCH 21/38] remove manage admin function, nullcheck for oidc_id, undo removal of * in method TableName --- pkg/models/teams.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 37917d4e2..a331c3fed 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -82,7 +82,7 @@ type TeamMember struct { } // TableName makes beautiful table names -func (TeamMember) TableName() string { +func (*TeamMember) TableName() string { return "team_members" } @@ -94,6 +94,7 @@ type TeamUser struct { TeamID int64 `json:"-"` } +// TeamData is the relevant data for a team and is delivered by oidc token type TeamData struct { TeamName string OidcID string @@ -166,7 +167,7 @@ func FindAllOidcTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err e Table("team_members"). Where("user_id = ? ", userID). Join("RIGHT", "teams", "teams.id = team_members.team_id"). - Where("teams.oidc_id != ?", ""). + Where("teams.oidc_id != ? AND teams.oidc_id IS NOT NULL", ""). Cols("teams.id"). Find(&ts) if ts == nil || err != nil { @@ -325,8 +326,7 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) { return } - var admin = true - tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: admin} + tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: true} if err = tm.Create(s, doer); err != nil { return err } @@ -337,11 +337,6 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) { }) } -func (t *Team) ManageAdminRight(teamMember TeamMember, admin bool) { - // Insert the current user as member and admin - teamMember.Admin = admin -} - // Delete deletes a team // @Summary Deletes a team // @Description Delets a team. This will also remove the access for all users in that team. From e1c0c4fa908aa6dcedb239bb9f65dadbdc3f2453 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 17:26:22 +0100 Subject: [PATCH 22/38] use models.TeamData instead of declaring struct twice --- pkg/modules/auth/openid/openid.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 7f57296a4..bbe0ee12c 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -59,11 +59,6 @@ type Provider struct { openIDProvider *oidc.Provider Oauth2Config *oauth2.Config `json:"-"` } -type TeamData struct { - TeamName string - OidcID string - Description string -} type claims struct { Email string `json:"email"` Name string `json:"name"` @@ -267,8 +262,8 @@ func SignOutFromOrDeleteTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64 } } -func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []TeamData, err error) { - teamData = []TeamData{} +func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []models.TeamData, err error) { + teamData = []models.TeamData{} if groups != nil { el := groups.([]interface{}) for _, data := range el { @@ -299,13 +294,13 @@ func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []Te log.Errorf("Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s", provider.Name) return teamData, &user.ErrOpenIDCustomScopeMalformed{} } - teamData = append(teamData, TeamData{TeamName: name, OidcID: oidcID, Description: description}) + teamData = append(teamData, models.TeamData{TeamName: name, OidcID: oidcID, Description: description}) } } return teamData, nil } -func CreateTeamWithData(s *xorm.Session, teamData TeamData, u *user.User) (team *models.Team, err error) { +func CreateTeamWithData(s *xorm.Session, teamData models.TeamData, u *user.User) (team *models.Team, err error) { tea := &models.Team{ Name: teamData.TeamName, Description: teamData.Description, @@ -318,7 +313,7 @@ func CreateTeamWithData(s *xorm.Session, teamData TeamData, u *user.User) (team } // this functions creates an array of existing teams that was generated from the oidc data. -func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []TeamData, u *user.User) (te []models.Team, err error) { +func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.TeamData, u *user.User) (te []models.Team, err error) { te = []models.Team{} // Procedure can only be successful if oidcID is set and converted to string for _, oidcTeam := range teamData { From 956d7971caeb659b4717d3eaec8ee126c64ef4af Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 18:48:28 +0100 Subject: [PATCH 23/38] move find x not in y for int64 slices to utils --- pkg/utils/slice_difference.go | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pkg/utils/slice_difference.go diff --git a/pkg/utils/slice_difference.go b/pkg/utils/slice_difference.go new file mode 100644 index 000000000..cc01fad10 --- /dev/null +++ b/pkg/utils/slice_difference.go @@ -0,0 +1,37 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package utils + +// find the elements which appear in slice1,but not in slice2 +func NotIn(slice1 []int64, slice2 []int64) []int64 { + var diff []int64 + + for _, s1 := range slice1 { + found := false + for _, s2 := range slice2 { + if s1 == s2 { + found = true + break + } + } + // int64 not found. We add it to return slice + if !found { + diff = append(diff, s1) + } + } + return diff +} From dace429bb76e81625255b1992a1d6784758a0283 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 19:52:18 +0100 Subject: [PATCH 24/38] rework openid.go, add errors to return, make team deletion more robust --- pkg/models/team_members.go | 13 +++-- pkg/modules/auth/openid/openid.go | 97 ++++++++++++++----------------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index ced46aeed..8af8aa59b 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -44,7 +44,6 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) { if err != nil { return err } - // Check if the user exists member, err := user2.GetUserByUsername(s, tm.Username) if err != nil { @@ -113,12 +112,18 @@ func (tm *TeamMember) CheckMembership(s *xorm.Session) (exists bool, err error) exists, err = s. Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). Get(&TeamMember{}) - if exists && err == nil { - return - } return exists, err } +func (tm *TeamMember) GetMemberCount(s *xorm.Session) (memberCount int, err error) { + members := []TeamMember{} + err = s. + Where("team_id = ?", tm.TeamID). + Cols("user_id"). + Find(&members) + return len(members), err +} + // Update toggles a team member's admin status // @Summary Toggle a team member's admin status // @Description If a user is team admin, this will make them member and vise-versa. diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index bbe0ee12c..b4f4e6904 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -20,24 +20,24 @@ import ( "context" "encoding/json" "errors" - "fmt" "math/rand" "net/http" + "strconv" "time" "code.vikunja.io/web/handler" "code.vikunja.io/api/pkg/db" - "xorm.io/xorm" - "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/user" + "code.vikunja.io/api/pkg/utils" "github.com/coreos/go-oidc/v3/oidc" petname "github.com/dustinkirkland/golang-petname" "github.com/labstack/echo/v4" "golang.org/x/oauth2" + "xorm.io/xorm" ) // Callback contains the callback after an auth request was made and redirected @@ -64,7 +64,6 @@ type claims struct { Name string `json:"name"` PreferredUsername string `json:"preferred_username"` Nickname string `json:"nickname"` - Teams []string `json:"groups"` VikunjaGroups interface{} `json:"vikunja_groups"` } @@ -193,7 +192,6 @@ func HandleCallback(c echo.Context) error { // Check if we have seen this user before u, err := getOrCreateUser(s, cl, idToken.Issuer, idToken.Subject) - if err != nil { _ = s.Rollback() log.Errorf("Error creating new user for provider %s: %v", provider.Name, err) @@ -201,14 +199,18 @@ func HandleCallback(c echo.Context) error { } // does the oidc token contain well formed "vikunja_groups" through vikunja_scope - teamData, err := getTeamDataFromToken(cl.VikunjaGroups, provider) - if err != nil { - log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) - return handler.HandleHTTPError(err, c) + teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, provider) + if len(errs) > 0 { + for _, err := range errs { + log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) + } } //find old teams for user through oidc - oldOidcTeams, _ := models.FindAllOidcTeamIDsForUser(s, u.ID) + oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID) + if err != nil { + log.Errorf("No Oidc Teams found for user", err) + } var oidcTeams []int64 if len(teamData) > 0 { // check if we have seen these teams before. @@ -220,20 +222,21 @@ func HandleCallback(c echo.Context) error { return err } for _, team := range teams { - tm := models.TeamMember{TeamID: team.ID, Username: u.Username} + tm := models.TeamMember{TeamID: team.ID, UserID: u.ID, Username: u.Username} exists, err := tm.CheckMembership(s) if !exists { err = tm.Create(s, u) if err != nil { - log.Debugf("Could not assign %v to %v. %v", u.Username, team.Name, err) + log.Errorf("Could not assign %v to %v. %v", u.Username, team.Name, err) } - } else { - log.Debugf("Team exists? %v or error: %v", exists, err) } oidcTeams = append(oidcTeams, team.ID) } } - SignOutFromOrDeleteTeamsByID(s, u, notIn(oldOidcTeams, oidcTeams)) + errs = SignOutFromOrDeleteTeamsByID(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) + for _, err := range errs { + log.Errorf("Found Error while signing out from teams %v", err) + } err = s.Commit() if err != nil { _ = s.Rollback() @@ -244,26 +247,35 @@ func HandleCallback(c echo.Context) error { return auth.NewUserAuthTokenResponse(u, c, false) } -func SignOutFromOrDeleteTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) { +func SignOutFromOrDeleteTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { + errs = []error{} for _, teamID := range teamIDs { - tm := models.TeamMember{TeamID: teamID, Username: u.Username} - err := tm.Delete(s, u) - if err != nil { + tm := models.TeamMember{TeamID: teamID, UserID: u.ID, Username: u.Username} + exists, err := tm.CheckMembership(s) + memberCount, _ := tm.GetMemberCount(s) + if !exists { + continue + } + err = tm.Delete(s, u) + // if you cannot delete the team_member + if err != nil || memberCount <= 1 { team, err := models.GetTeamByID(s, teamID) if err != nil { - log.Errorf("Cannot find team with id: %v, err: %v", teamID, err) - } else { - err = team.Delete(s, u) - if err != nil { - log.Errorf("Cannot delete team %v", err) - } + errs = append(errs, err) + continue + } + err = team.Delete(s, u) + if err != nil { + errs = append(errs, err) } } } + return errs } -func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []models.TeamData, err error) { +func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []models.TeamData, errs []error) { teamData = []models.TeamData{} + errs = []error{} if groups != nil { el := groups.([]interface{}) for _, data := range el { @@ -280,24 +292,24 @@ func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []mo if team["oidcID"] != nil { switch t := team["oidcID"].(type) { case int64: - oidcID = fmt.Sprintf("%v", team["oidcID"]) + oidcID = strconv.FormatInt(team["oidcID"].(int64), 10) case string: oidcID = string(team["oidcID"].(string)) case float64: - fl := float64(team["oidcID"].(float64)) - oidcID = fmt.Sprintf("%v", fl) + oidcID = strconv.FormatFloat(team["oidcID"].(float64), 'f', -1, 64) default: log.Errorf("No oidcID assigned for %v or type %v not supported", team, t) } } if name == "" || oidcID == "" { log.Errorf("Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s", provider.Name) - return teamData, &user.ErrOpenIDCustomScopeMalformed{} + errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{}) + continue } teamData = append(teamData, models.TeamData{TeamName: name, OidcID: oidcID, Description: description}) } } - return teamData, nil + return teamData, errs } func CreateTeamWithData(s *xorm.Session, teamData models.TeamData, u *user.User) (team *models.Team, err error) { @@ -306,8 +318,6 @@ func CreateTeamWithData(s *xorm.Session, teamData models.TeamData, u *user.User) Description: teamData.Description, OidcID: teamData.OidcID, } - log.Debugf("Teams: %v", tea.OidcID) - err = tea.Create(s, u) return tea, err } @@ -331,7 +341,6 @@ func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.TeamData, } } - log.Debugf("Array: %v", te) return te, err } @@ -407,23 +416,3 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us return } - -// find the elements which appear in slice1,but not in slice2 -func notIn(slice1 []int64, slice2 []int64) []int64 { - var diff []int64 - - for _, s1 := range slice1 { - found := false - for _, s2 := range slice2 { - if s1 == s2 { - found = true - break - } - } - // String not found. We add it to return slice - if !found { - diff = append(diff, s1) - } - } - return diff -} From bd2cb9ad8b696558d4414f9df2c993ad39121ba8 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 20:04:25 +0100 Subject: [PATCH 25/38] make documentation cleaner --- config.yml.sample | 2 +- pkg/modules/auth/openid/openid.md | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/config.yml.sample b/config.yml.sample index 209a29d33..ef8f87d1b 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -304,7 +304,7 @@ auth: # The client secret used to authenticate Vikunja at the OpenID Connect provider. clientsecret: # The scope necessary to use oidc. - # If you want to use the Feature to create and assign to vikunja teams via oidc, you have to add the custom "vikunja_scope". + # If you want to use the Feature to create and assign to vikunja teams via oidc, you have to add the custom "vikunja_scope" and check [openid.md](https://kolaente.dev/vikunja/api/src/branch/main/pkg/modules/auth/openid/openid.md) # e.g. scope: openid email profile vikunja_scope scope: openid email profile diff --git a/pkg/modules/auth/openid/openid.md b/pkg/modules/auth/openid/openid.md index c3db48822..c63a89c22 100644 --- a/pkg/modules/auth/openid/openid.md +++ b/pkg/modules/auth/openid/openid.md @@ -1,14 +1,12 @@ -regarding: -https://kolaente.dev/vikunja/api/pulls/1279 - # Assign teams via oidc This PR adds the functionality to assign users to teams via oidc. Read carefully and brief your administrators to use this feature. Tested with oidc provider authentik. -To distinguish between groups created in vikunja and groups generated via oidc, there is an attribute neccessary, which is called: *oidcID* +To distinguish between teams created in vikunja and teams generated via oidc, an attribute for vikunja teams is introduced, which is called: *oidcID* ## Setup -Edit config.yml to include scope: openid profile email vikunja_scope + +Edit [config.yml](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) to include scope: openid profile email vikunja_scope For authentik to use group assignment feature: - go to: .../if/admin/#/core/property-mappings @@ -51,12 +49,11 @@ You should see "the description you entered in the oidc provider's admin area" ## IMPORTANT NOTES: -**SSO/OIDC teams cannot be edited.** +* **SSO/OIDC teams cannot be edited.** -**It is crucial to call the element "vikunja_groups" since this is the name vikunja is looking for.** - -**Additionally, make sure to deliver an "oidcID" and a "name".** +* **It is crucial to call the element "vikunja_groups" since this is the name vikunja is looking for.** +* **Additionally, make sure to deliver an "oidcID" and a "name".** From 82b7c14fed965fd4e832c8fbd59127ac09f6ce45 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 13 Feb 2023 20:43:24 +0100 Subject: [PATCH 26/38] fix lint --- pkg/models/error.go | 4 +-- pkg/modules/auth/openid/openid.go | 60 ++++++++++++++++--------------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/pkg/models/error.go b/pkg/models/error.go index c343ad38c..80f3832f0 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1214,7 +1214,7 @@ func (err ErrTeamsDoNotExist) HTTPError() web.HTTPError { // ErrOIDCTeamDoesNotExist represents an error where a team with specified name and specified oidcId property does not exist type ErrOIDCTeamDoesNotExist struct { - OidcId string + OidcID string Name string } @@ -1225,7 +1225,7 @@ func IsErrOIDCTeamDoesNotExist(err error) bool { } func (err ErrOIDCTeamDoesNotExist) Error() string { - return fmt.Sprintf("No Team with that name and valid property oidcId could be found [Team Name: %v] [OidcId : %v] ", err.Name, err.OidcId) + return fmt.Sprintf("No Team with that name and valid property oidcId could be found [Team Name: %v] [OidcId : %v] ", err.Name, err.OidcID) } // ErrCodeTeamDoesNotExist holds the unique world-error code of this error diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index b4f4e6904..547a5075c 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -200,38 +200,18 @@ func HandleCallback(c echo.Context) error { // does the oidc token contain well formed "vikunja_groups" through vikunja_scope teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, provider) - if len(errs) > 0 { - for _, err := range errs { - log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) - } + for _, err := range errs { + log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) } //find old teams for user through oidc oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID) if err != nil { - log.Errorf("No Oidc Teams found for user", err) + log.Errorf("No Oidc Teams found for user %v", err) } - var oidcTeams []int64 - if len(teamData) > 0 { - // check if we have seen these teams before. - // find or create Teams and assign user as teammember. - log.Debugf("TeamData is set %v", teamData) - teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) - if err != nil { - log.Errorf("Error verifying team for name %v, got %v", cl.Name, teams, err) - return err - } - for _, team := range teams { - tm := models.TeamMember{TeamID: team.ID, UserID: u.ID, Username: u.Username} - exists, err := tm.CheckMembership(s) - if !exists { - err = tm.Create(s, u) - if err != nil { - log.Errorf("Could not assign %v to %v. %v", u.Username, team.Name, err) - } - } - oidcTeams = append(oidcTeams, team.ID) - } + oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData) + if err != nil { + log.Errorf("Could not proceed with group routine %v", err) } errs = SignOutFromOrDeleteTeamsByID(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) for _, err := range errs { @@ -247,16 +227,40 @@ func HandleCallback(c echo.Context) error { return auth.NewUserAuthTokenResponse(u, c, false) } +func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models.TeamData) (oidcTeams []int64, err error) { + if len(teamData) > 0 { + // check if we have seen these teams before. + // find or create Teams and assign user as teammember. + teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) + if err != nil { + log.Errorf("Error verifying team for %v, got %v. Error: %v", u.Name, teams, err) + return nil, err + } + for _, team := range teams { + tm := models.TeamMember{TeamID: team.ID, UserID: u.ID, Username: u.Username} + exists, _ := tm.CheckMembership(s) + if !exists { + err = tm.Create(s, u) + if err != nil { + log.Errorf("Could not assign %v to %v. %v", u.Username, team.Name, err) + } + } + oidcTeams = append(oidcTeams, team.ID) + } + } + return oidcTeams, err + +} func SignOutFromOrDeleteTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { errs = []error{} for _, teamID := range teamIDs { tm := models.TeamMember{TeamID: teamID, UserID: u.ID, Username: u.Username} - exists, err := tm.CheckMembership(s) + exists, _ := tm.CheckMembership(s) memberCount, _ := tm.GetMemberCount(s) if !exists { continue } - err = tm.Delete(s, u) + err := tm.Delete(s, u) // if you cannot delete the team_member if err != nil || memberCount <= 1 { team, err := models.GetTeamByID(s, teamID) From d3ca751f4c9a9917a130897f4ab11de9062fef55 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 15:23:32 +0100 Subject: [PATCH 27/38] undo team gets deleted if user is last team member remove logic behind deleting last team_member --- pkg/models/team_members.go | 10 +++++----- pkg/modules/auth/openid/openid.go | 27 ++++++++++++--------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index 8af8aa59b..49e41f247 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -115,13 +115,13 @@ func (tm *TeamMember) CheckMembership(s *xorm.Session) (exists bool, err error) return exists, err } -func (tm *TeamMember) GetMemberCount(s *xorm.Session) (memberCount int, err error) { - members := []TeamMember{} - err = s. +func (tm *TeamMember) GetMemberCount(s *xorm.Session) (memberCount int64, err error) { + member := TeamMember{} + memberCount, err = s. Where("team_id = ?", tm.TeamID). Cols("user_id"). - Find(&members) - return len(members), err + Count(&member) + return memberCount, err } // Update toggles a team member's admin status diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 547a5075c..eeefde3d3 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -213,7 +213,8 @@ func HandleCallback(c echo.Context) error { if err != nil { log.Errorf("Could not proceed with group routine %v", err) } - errs = SignOutFromOrDeleteTeamsByID(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) + errs = SignOutFromTeamsByID(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) + log.Errorf("%v", errs) for _, err := range errs { log.Errorf("Found Error while signing out from teams %v", err) } @@ -251,27 +252,23 @@ func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models. return oidcTeams, err } -func SignOutFromOrDeleteTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { +func SignOutFromTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { errs = []error{} for _, teamID := range teamIDs { tm := models.TeamMember{TeamID: teamID, UserID: u.ID, Username: u.Username} - exists, _ := tm.CheckMembership(s) - memberCount, _ := tm.GetMemberCount(s) + exists, err := tm.CheckMembership(s) + if err != nil { + errs = append(errs, err) + continue + } if !exists { continue } - err := tm.Delete(s, u) + err = tm.Delete(s, u) // if you cannot delete the team_member - if err != nil || memberCount <= 1 { - team, err := models.GetTeamByID(s, teamID) - if err != nil { - errs = append(errs, err) - continue - } - err = team.Delete(s, u) - if err != nil { - errs = append(errs, err) - } + if err != nil { + errs = append(errs, err) + continue } } return errs From 584fa51a2ab666e6c13263ef774292f9f0983255 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 15:48:38 +0100 Subject: [PATCH 28/38] change get to exist in team_members.go:114 --- pkg/models/team_members.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index 49e41f247..fbc38d24d 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -111,7 +111,7 @@ func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) { func (tm *TeamMember) CheckMembership(s *xorm.Session) (exists bool, err error) { exists, err = s. Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID). - Get(&TeamMember{}) + Exist(&TeamMember{}) return exists, err } From 183b870c6dbc4bc9e887b819074c9530702c45ba Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 15:52:16 +0100 Subject: [PATCH 29/38] add punctuation and comments for errors --- docs/content/doc/usage/errors.md | 6 +++--- pkg/models/error.go | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index 9dabb288c..bc8a332d9 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -111,9 +111,9 @@ This document describes the different errors Vikunja can return. | 6005 | 409 | The user is already a member of that team. | | 6006 | 400 | Cannot delete the last team member. | | 6007 | 403 | The team does not have access to the list to perform that action. | -| 6008 | 400 | There are no teams found with that team name | -| 6009 | 400 | There is no oidc team with that team name and oidcId | -| 6010 | 400 | There are no oidc teams found for the user | +| 6008 | 400 | There are no teams found with that team name. | +| 6009 | 400 | There is no oidc team with that team name and oidcId. | +| 6010 | 400 | There are no oidc teams found for the user. | diff --git a/pkg/models/error.go b/pkg/models/error.go index 80f3832f0..7efaca871 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1190,6 +1190,7 @@ func (err ErrTeamDoesNotHaveAccessToList) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."} } +// ErrTeamsDoNotExist represents an error where Teams searched via non-unique name do not exist. type ErrTeamsDoNotExist struct { Name string } @@ -1224,6 +1225,7 @@ func IsErrOIDCTeamDoesNotExist(err error) bool { return ok } +// ErrTeamDoesNotExist represents an error where a team does not exist func (err ErrOIDCTeamDoesNotExist) Error() string { return fmt.Sprintf("No Team with that name and valid property oidcId could be found [Team Name: %v] [OidcId : %v] ", err.Name, err.OidcID) } From fc89564ebcc2d19dc7185be65851f4630cc3471d Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 16:17:59 +0100 Subject: [PATCH 30/38] cast VikujGroups directly to []map[string]interface{} changug []models.Team to []*models.Team --- pkg/modules/auth/openid/openid.go | 79 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index eeefde3d3..dc87d9668 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -60,11 +60,11 @@ type Provider struct { Oauth2Config *oauth2.Config `json:"-"` } type claims struct { - Email string `json:"email"` - Name string `json:"name"` - PreferredUsername string `json:"preferred_username"` - Nickname string `json:"nickname"` - VikunjaGroups interface{} `json:"vikunja_groups"` + Email string `json:"email"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Nickname string `json:"nickname"` + VikunjaGroups []map[string]interface{} `json:"vikunja_groups"` } func init() { @@ -274,41 +274,37 @@ func SignOutFromTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs return errs } -func getTeamDataFromToken(groups interface{}, provider *Provider) (teamData []models.TeamData, errs []error) { +func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (teamData []models.TeamData, errs []error) { teamData = []models.TeamData{} errs = []error{} - if groups != nil { - el := groups.([]interface{}) - for _, data := range el { - team := data.(map[string]interface{}) - var name string - var description string - var oidcID string - if team["name"] != nil { - name = team["name"].(string) - } - if team["description"] != nil { - description = team["description"].(string) - } - if team["oidcID"] != nil { - switch t := team["oidcID"].(type) { - case int64: - oidcID = strconv.FormatInt(team["oidcID"].(int64), 10) - case string: - oidcID = string(team["oidcID"].(string)) - case float64: - oidcID = strconv.FormatFloat(team["oidcID"].(float64), 'f', -1, 64) - default: - log.Errorf("No oidcID assigned for %v or type %v not supported", team, t) - } - } - if name == "" || oidcID == "" { - log.Errorf("Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s", provider.Name) - errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{}) - continue - } - teamData = append(teamData, models.TeamData{TeamName: name, OidcID: oidcID, Description: description}) + for _, team := range groups { + var name string + var description string + var oidcID string + if team["name"] != nil { + name = team["name"].(string) } + if team["description"] != nil { + description = team["description"].(string) + } + if team["oidcID"] != nil { + switch t := team["oidcID"].(type) { + case int64: + oidcID = strconv.FormatInt(team["oidcID"].(int64), 10) + case string: + oidcID = string(team["oidcID"].(string)) + case float64: + oidcID = strconv.FormatFloat(team["oidcID"].(float64), 'f', -1, 64) + default: + log.Errorf("No oidcID assigned for %v or type %v not supported", team, t) + } + } + if name == "" || oidcID == "" { + log.Errorf("Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s", provider.Name) + errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{}) + continue + } + teamData = append(teamData, models.TeamData{TeamName: name, OidcID: oidcID, Description: description}) } return teamData, errs } @@ -324,8 +320,8 @@ func CreateTeamWithData(s *xorm.Session, teamData models.TeamData, u *user.User) } // this functions creates an array of existing teams that was generated from the oidc data. -func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.TeamData, u *user.User) (te []models.Team, err error) { - te = []models.Team{} +func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.TeamData, u *user.User) (te []*models.Team, err error) { + te = []*models.Team{} // Procedure can only be successful if oidcID is set and converted to string for _, oidcTeam := range teamData { team, err := models.GetTeamByOidcIDAndName(s, oidcTeam.OidcID, oidcTeam.TeamName) @@ -335,12 +331,11 @@ func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.TeamData, if err != nil { return te, err } - te = append(te, *newTeam) + te = append(te, newTeam) } else { log.Debugf("Team with oidc_id %v and name %v already exists.", team.OidcID, team.Name) - te = append(te, team) + te = append(te, &team) } - } return te, err } From b607fea49d5cb31bd966dfd2bb292f8eea5a5d74 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 16:32:20 +0100 Subject: [PATCH 31/38] change too generic name TeamData struct to OIDCTeamData --- pkg/models/teams.go | 4 ++-- pkg/modules/auth/openid/openid.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index a331c3fed..9d1586dda 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -94,8 +94,8 @@ type TeamUser struct { TeamID int64 `json:"-"` } -// TeamData is the relevant data for a team and is delivered by oidc token -type TeamData struct { +// OIDCTeamData is the relevant data for a team and is delivered by oidc token +type OIDCTeamData struct { TeamName string OidcID string Description string diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index dc87d9668..b35f702fe 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -228,7 +228,7 @@ func HandleCallback(c echo.Context) error { return auth.NewUserAuthTokenResponse(u, c, false) } -func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models.TeamData) (oidcTeams []int64, err error) { +func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models.OIDCTeamData) (oidcTeams []int64, err error) { if len(teamData) > 0 { // check if we have seen these teams before. // find or create Teams and assign user as teammember. @@ -274,8 +274,8 @@ func SignOutFromTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs return errs } -func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (teamData []models.TeamData, errs []error) { - teamData = []models.TeamData{} +func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (teamData []models.OIDCTeamData, errs []error) { + teamData = []models.OIDCTeamData{} errs = []error{} for _, team := range groups { var name string @@ -304,12 +304,12 @@ func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) ( errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{}) continue } - teamData = append(teamData, models.TeamData{TeamName: name, OidcID: oidcID, Description: description}) + teamData = append(teamData, models.OIDCTeamData{TeamName: name, OidcID: oidcID, Description: description}) } return teamData, errs } -func CreateTeamWithData(s *xorm.Session, teamData models.TeamData, u *user.User) (team *models.Team, err error) { +func CreateTeamWithData(s *xorm.Session, teamData models.OIDCTeamData, u *user.User) (team *models.Team, err error) { tea := &models.Team{ Name: teamData.TeamName, Description: teamData.Description, @@ -320,7 +320,7 @@ func CreateTeamWithData(s *xorm.Session, teamData models.TeamData, u *user.User) } // this functions creates an array of existing teams that was generated from the oidc data. -func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.TeamData, u *user.User) (te []*models.Team, err error) { +func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.OIDCTeamData, u *user.User) (te []*models.Team, err error) { te = []*models.Team{} // Procedure can only be successful if oidcID is set and converted to string for _, oidcTeam := range teamData { From 169b668c3cb7c590be62e094ae1f78c0243036d0 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 10 Mar 2023 13:46:18 +0100 Subject: [PATCH 32/38] remove left over function GetMemberCount, rename function SignOut to RemoveFrom --- pkg/models/team_members.go | 9 --------- pkg/modules/auth/openid/openid.go | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/pkg/models/team_members.go b/pkg/models/team_members.go index fbc38d24d..6fcf42274 100644 --- a/pkg/models/team_members.go +++ b/pkg/models/team_members.go @@ -115,15 +115,6 @@ func (tm *TeamMember) CheckMembership(s *xorm.Session) (exists bool, err error) return exists, err } -func (tm *TeamMember) GetMemberCount(s *xorm.Session) (memberCount int64, err error) { - member := TeamMember{} - memberCount, err = s. - Where("team_id = ?", tm.TeamID). - Cols("user_id"). - Count(&member) - return memberCount, err -} - // Update toggles a team member's admin status // @Summary Toggle a team member's admin status // @Description If a user is team admin, this will make them member and vise-versa. diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index b35f702fe..504810e81 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -213,7 +213,7 @@ func HandleCallback(c echo.Context) error { if err != nil { log.Errorf("Could not proceed with group routine %v", err) } - errs = SignOutFromTeamsByID(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) + errs = RemoveUserFromTeamsByIds(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) log.Errorf("%v", errs) for _, err := range errs { log.Errorf("Found Error while signing out from teams %v", err) @@ -252,7 +252,7 @@ func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models. return oidcTeams, err } -func SignOutFromTeamsByID(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { +func RemoveUserFromTeamsByIds(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { errs = []error{} for _, teamID := range teamIDs { tm := models.TeamMember{TeamID: teamID, UserID: u.ID, Username: u.Username} From 85307ce666d28e2e8cd50045337b290d265879b5 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 30 Mar 2023 17:15:07 +0200 Subject: [PATCH 33/38] add swagger files --- pkg/swagger/docs.go | 439 +++++++++++++++++++++++---------------- pkg/swagger/swagger.json | 436 +++++++++++++++++++++++--------------- pkg/swagger/swagger.yaml | 252 +++++++++++----------- 3 files changed, 649 insertions(+), 478 deletions(-) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 1274e01b0..ff9875400 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,5 +1,4 @@ -// Package swagger GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT package swagger import "github.com/swaggo/swag" @@ -7620,36 +7619,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who initially created the bucket.", - "$ref": "#/definitions/user.User" - }, - "filter_by": { - "description": "The field name of the field to filter by", - "type": "array", - "items": { - "type": "string" - } - }, - "filter_comparator": { - "description": "The comparator for field and value", - "type": "array", - "items": { - "type": "string" - } - }, - "filter_concat": { - "description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,", - "type": "string" - }, - "filter_include_nulls": { - "description": "If set to true, the result will also include null values", - "type": "boolean" - }, - "filter_value": { - "description": "The value of the field name to filter by", - "type": "array", - "items": { - "type": "string" - } + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "id": { "description": "The unique, numeric id of this bucket.", @@ -7668,24 +7642,10 @@ const docTemplate = `{ "description": "The list this bucket belongs to.", "type": "integer" }, - "order_by": { - "description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.", - "type": "array", - "items": { - "type": "string" - } - }, "position": { "description": "The position this bucket has when querying all buckets. See the tasks.position property on how to use this.", "type": "number" }, - "sort_by": { - "description": "The query parameter to sort by. This is for ex. done, priority, etc.", - "type": "array", - "items": { - "type": "string" - } - }, "tasks": { "description": "All tasks which belong to this bucket.", "type": "array", @@ -7701,9 +7661,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this bucket was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.BulkAssignees": { @@ -7715,9 +7673,7 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/user.User" } - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.BulkTask": { @@ -7751,7 +7707,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who initially created the task.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The task description.", @@ -7823,7 +7783,11 @@ const docTemplate = `{ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "$ref": "#/definitions/models.RelatedTaskMap" + "allOf": [ + { + "$ref": "#/definitions/models.RelatedTaskMap" + } + ] }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -7838,7 +7802,11 @@ const docTemplate = `{ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "type": "integer" + "allOf": [ + { + "$ref": "#/definitions/models.TaskRepeatMode" + } + ] }, "start_date": { "description": "When this task starts.", @@ -7846,7 +7814,11 @@ const docTemplate = `{ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "task_ids": { "description": "A list of task ids to update", @@ -7863,9 +7835,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.DatabaseNotifications": { @@ -7893,9 +7863,7 @@ const docTemplate = `{ "read_at": { "description": "When this notification is marked as read, this will be updated with the current timestamp.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.Label": { @@ -7907,7 +7875,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this label", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The label description.", @@ -7931,9 +7903,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this label was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.LabelTask": { @@ -7946,9 +7916,7 @@ const docTemplate = `{ "label_id": { "description": "The label id you want to associate with a task.", "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.LabelTaskBulk": { @@ -7960,9 +7928,7 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/models.Label" } - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.LinkSharing": { @@ -7990,26 +7956,36 @@ const docTemplate = `{ }, "right": { "description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "shared_by": { "description": "The user who shared this list", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "sharing_type": { "description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.SharingType" + } + ] }, "updated": { "description": "A timestamp when this share was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.List": { @@ -8058,7 +8034,11 @@ const docTemplate = `{ }, "owner": { "description": "The user who created this list.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "position": { "description": "The position this list has when querying all lists. See the tasks.position property on how to use this.", @@ -8066,7 +8046,11 @@ const docTemplate = `{ }, "subscription": { "description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The title of the list. You'll see this in the namespace overview.", @@ -8077,9 +8061,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this list was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.ListDuplicate": { @@ -8087,14 +8069,16 @@ const docTemplate = `{ "properties": { "list": { "description": "The copied list", - "$ref": "#/definitions/models.List" + "allOf": [ + { + "$ref": "#/definitions/models.List" + } + ] }, "namespace_id": { "description": "The target namespace ID", "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.ListUser": { @@ -8110,9 +8094,13 @@ const docTemplate = `{ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8121,9 +8109,7 @@ const docTemplate = `{ "user_id": { "description": "The username.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.Message": { @@ -8161,11 +8147,19 @@ const docTemplate = `{ }, "owner": { "description": "The user who owns this namespace", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The name of this namespace.", @@ -8176,9 +8170,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.NamespaceUser": { @@ -8194,9 +8186,13 @@ const docTemplate = `{ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8205,9 +8201,7 @@ const docTemplate = `{ "user_id": { "description": "The username.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.NamespaceWithLists": { @@ -8242,11 +8236,19 @@ const docTemplate = `{ }, "owner": { "description": "The user who owns this namespace", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The name of this namespace.", @@ -8257,9 +8259,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.RelatedTaskMap": { @@ -8271,6 +8271,50 @@ const docTemplate = `{ } } }, + "models.RelationKind": { + "type": "string", + "enum": [ + "unknown", + "subtask", + "parenttask", + "related", + "duplicateof", + "duplicates", + "blocking", + "blocked", + "precedes", + "follows", + "copiedfrom", + "copiedto" + ], + "x-enum-varnames": [ + "RelationKindUnknown", + "RelationKindSubtask", + "RelationKindParenttask", + "RelationKindRelated", + "RelationKindDuplicateOf", + "RelationKindDuplicates", + "RelationKindBlocking", + "RelationKindBlocked", + "RelationKindPreceeds", + "RelationKindFollows", + "RelationKindCopiedFrom", + "RelationKindCopiedTo" + ] + }, + "models.Right": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "RightRead", + "RightWrite", + "RightAdmin" + ] + }, "models.SavedFilter": { "type": "object", "properties": { @@ -8284,7 +8328,11 @@ const docTemplate = `{ }, "filters": { "description": "The actual filters this filter contains", - "$ref": "#/definitions/models.TaskCollection" + "allOf": [ + { + "$ref": "#/definitions/models.TaskCollection" + } + ] }, "id": { "description": "The unique numeric id of this saved filter", @@ -8296,7 +8344,11 @@ const docTemplate = `{ }, "owner": { "description": "The user who owns this filter", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "title": { "description": "The title of the filter.", @@ -8307,11 +8359,22 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this filter was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, + "models.SharingType": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "SharingTypeUnknown", + "SharingTypeWithoutPassword", + "SharingTypeWithPassword" + ] + }, "models.Subscription": { "type": "object", "properties": { @@ -8332,10 +8395,12 @@ const docTemplate = `{ }, "user": { "description": "The user who made this subscription", - "$ref": "#/definitions/user.User" - }, - "web.CRUDable": {}, - "web.Rights": {} + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] + } } }, "models.Task": { @@ -8369,7 +8434,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who initially created the task.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The task description.", @@ -8441,7 +8510,11 @@ const docTemplate = `{ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "$ref": "#/definitions/models.RelatedTaskMap" + "allOf": [ + { + "$ref": "#/definitions/models.RelatedTaskMap" + } + ] }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -8456,7 +8529,11 @@ const docTemplate = `{ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "type": "integer" + "allOf": [ + { + "$ref": "#/definitions/models.TaskRepeatMode" + } + ] }, "start_date": { "description": "When this task starts.", @@ -8464,7 +8541,11 @@ const docTemplate = `{ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The task text. This is what you'll see in the list.", @@ -8474,9 +8555,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskAssginee": { @@ -8487,9 +8566,7 @@ const docTemplate = `{ }, "user_id": { "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskAttachment": { @@ -8509,9 +8586,7 @@ const docTemplate = `{ }, "task_id": { "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskCollection": { @@ -8559,9 +8634,7 @@ const docTemplate = `{ "items": { "type": "string" } - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskComment": { @@ -8581,9 +8654,7 @@ const docTemplate = `{ }, "updated": { "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskRelation": { @@ -8595,7 +8666,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this relation", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "other_task_id": { "description": "The ID of the other task, the task which is being related.", @@ -8603,16 +8678,31 @@ const docTemplate = `{ }, "relation_kind": { "description": "The kind of the relation.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/models.RelationKind" + } + ] }, "task_id": { "description": "The ID of the \"base\" task, the task which has a relation to another.", "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, + "models.TaskRepeatMode": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "TaskRepeatModeDefault", + "TaskRepeatModeMonth", + "TaskRepeatModeFromCurrentDate" + ] + }, "models.Team": { "type": "object", "properties": { @@ -8622,7 +8712,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this team.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The team's description.", @@ -8653,9 +8747,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamList": { @@ -8671,9 +8763,13 @@ const docTemplate = `{ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "team_id": { "description": "The team id.", @@ -8682,9 +8778,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamMember": { @@ -8705,9 +8799,7 @@ const docTemplate = `{ "username": { "description": "The username of the member. We use this to prevent automated user id entering.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamNamespace": { @@ -8723,9 +8815,13 @@ const docTemplate = `{ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "team_id": { "description": "The team id.", @@ -8734,9 +8830,7 @@ const docTemplate = `{ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamUser": { @@ -8772,8 +8866,7 @@ const docTemplate = `{ "type": "string", "maxLength": 250, "minLength": 1 - }, - "web.Auth": {} + } } }, "models.TeamWithRight": { @@ -8785,7 +8878,11 @@ const docTemplate = `{ }, "created_by": { "description": "The user who created this team.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The team's description.", @@ -8814,16 +8911,12 @@ const docTemplate = `{ "maxLength": 250 }, "right": { - "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", - "default": 0 + "$ref": "#/definitions/models.Right" }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.UserWithRight": { @@ -8847,9 +8940,7 @@ const docTemplate = `{ "type": "string" }, "right": { - "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", - "default": 0 + "$ref": "#/definitions/models.Right" }, "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", @@ -8860,8 +8951,7 @@ const docTemplate = `{ "type": "string", "maxLength": 250, "minLength": 1 - }, - "web.Auth": {} + } } }, "notifications.DatabaseNotification": { @@ -9096,8 +9186,7 @@ const docTemplate = `{ "type": "string", "maxLength": 250, "minLength": 1 - }, - "web.Auth": {} + } } }, "v1.LinkShareAuth": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index f2fe657a0..926da7d25 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7611,36 +7611,11 @@ }, "created_by": { "description": "The user who initially created the bucket.", - "$ref": "#/definitions/user.User" - }, - "filter_by": { - "description": "The field name of the field to filter by", - "type": "array", - "items": { - "type": "string" - } - }, - "filter_comparator": { - "description": "The comparator for field and value", - "type": "array", - "items": { - "type": "string" - } - }, - "filter_concat": { - "description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,", - "type": "string" - }, - "filter_include_nulls": { - "description": "If set to true, the result will also include null values", - "type": "boolean" - }, - "filter_value": { - "description": "The value of the field name to filter by", - "type": "array", - "items": { - "type": "string" - } + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "id": { "description": "The unique, numeric id of this bucket.", @@ -7659,24 +7634,10 @@ "description": "The list this bucket belongs to.", "type": "integer" }, - "order_by": { - "description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.", - "type": "array", - "items": { - "type": "string" - } - }, "position": { "description": "The position this bucket has when querying all buckets. See the tasks.position property on how to use this.", "type": "number" }, - "sort_by": { - "description": "The query parameter to sort by. This is for ex. done, priority, etc.", - "type": "array", - "items": { - "type": "string" - } - }, "tasks": { "description": "All tasks which belong to this bucket.", "type": "array", @@ -7692,9 +7653,7 @@ "updated": { "description": "A timestamp when this bucket was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.BulkAssignees": { @@ -7706,9 +7665,7 @@ "items": { "$ref": "#/definitions/user.User" } - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.BulkTask": { @@ -7742,7 +7699,11 @@ }, "created_by": { "description": "The user who initially created the task.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The task description.", @@ -7814,7 +7775,11 @@ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "$ref": "#/definitions/models.RelatedTaskMap" + "allOf": [ + { + "$ref": "#/definitions/models.RelatedTaskMap" + } + ] }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -7829,7 +7794,11 @@ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "type": "integer" + "allOf": [ + { + "$ref": "#/definitions/models.TaskRepeatMode" + } + ] }, "start_date": { "description": "When this task starts.", @@ -7837,7 +7806,11 @@ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "task_ids": { "description": "A list of task ids to update", @@ -7854,9 +7827,7 @@ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.DatabaseNotifications": { @@ -7884,9 +7855,7 @@ "read_at": { "description": "When this notification is marked as read, this will be updated with the current timestamp.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.Label": { @@ -7898,7 +7867,11 @@ }, "created_by": { "description": "The user who created this label", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The label description.", @@ -7922,9 +7895,7 @@ "updated": { "description": "A timestamp when this label was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.LabelTask": { @@ -7937,9 +7908,7 @@ "label_id": { "description": "The label id you want to associate with a task.", "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.LabelTaskBulk": { @@ -7951,9 +7920,7 @@ "items": { "$ref": "#/definitions/models.Label" } - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.LinkSharing": { @@ -7981,26 +7948,36 @@ }, "right": { "description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "shared_by": { "description": "The user who shared this list", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "sharing_type": { "description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.SharingType" + } + ] }, "updated": { "description": "A timestamp when this share was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.List": { @@ -8049,7 +8026,11 @@ }, "owner": { "description": "The user who created this list.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "position": { "description": "The position this list has when querying all lists. See the tasks.position property on how to use this.", @@ -8057,7 +8038,11 @@ }, "subscription": { "description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The title of the list. You'll see this in the namespace overview.", @@ -8068,9 +8053,7 @@ "updated": { "description": "A timestamp when this list was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.ListDuplicate": { @@ -8078,14 +8061,16 @@ "properties": { "list": { "description": "The copied list", - "$ref": "#/definitions/models.List" + "allOf": [ + { + "$ref": "#/definitions/models.List" + } + ] }, "namespace_id": { "description": "The target namespace ID", "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.ListUser": { @@ -8101,9 +8086,13 @@ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8112,9 +8101,7 @@ "user_id": { "description": "The username.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.Message": { @@ -8152,11 +8139,19 @@ }, "owner": { "description": "The user who owns this namespace", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The name of this namespace.", @@ -8167,9 +8162,7 @@ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.NamespaceUser": { @@ -8185,9 +8178,13 @@ }, "right": { "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", @@ -8196,9 +8193,7 @@ "user_id": { "description": "The username.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.NamespaceWithLists": { @@ -8233,11 +8228,19 @@ }, "owner": { "description": "The user who owns this namespace", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "subscription": { "description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The name of this namespace.", @@ -8248,9 +8251,7 @@ "updated": { "description": "A timestamp when this namespace was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.RelatedTaskMap": { @@ -8262,6 +8263,50 @@ } } }, + "models.RelationKind": { + "type": "string", + "enum": [ + "unknown", + "subtask", + "parenttask", + "related", + "duplicateof", + "duplicates", + "blocking", + "blocked", + "precedes", + "follows", + "copiedfrom", + "copiedto" + ], + "x-enum-varnames": [ + "RelationKindUnknown", + "RelationKindSubtask", + "RelationKindParenttask", + "RelationKindRelated", + "RelationKindDuplicateOf", + "RelationKindDuplicates", + "RelationKindBlocking", + "RelationKindBlocked", + "RelationKindPreceeds", + "RelationKindFollows", + "RelationKindCopiedFrom", + "RelationKindCopiedTo" + ] + }, + "models.Right": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "RightRead", + "RightWrite", + "RightAdmin" + ] + }, "models.SavedFilter": { "type": "object", "properties": { @@ -8275,7 +8320,11 @@ }, "filters": { "description": "The actual filters this filter contains", - "$ref": "#/definitions/models.TaskCollection" + "allOf": [ + { + "$ref": "#/definitions/models.TaskCollection" + } + ] }, "id": { "description": "The unique numeric id of this saved filter", @@ -8287,7 +8336,11 @@ }, "owner": { "description": "The user who owns this filter", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "title": { "description": "The title of the filter.", @@ -8298,11 +8351,22 @@ "updated": { "description": "A timestamp when this filter was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, + "models.SharingType": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "SharingTypeUnknown", + "SharingTypeWithoutPassword", + "SharingTypeWithPassword" + ] + }, "models.Subscription": { "type": "object", "properties": { @@ -8323,10 +8387,12 @@ }, "user": { "description": "The user who made this subscription", - "$ref": "#/definitions/user.User" - }, - "web.CRUDable": {}, - "web.Rights": {} + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] + } } }, "models.Task": { @@ -8360,7 +8426,11 @@ }, "created_by": { "description": "The user who initially created the task.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The task description.", @@ -8432,7 +8502,11 @@ }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", - "$ref": "#/definitions/models.RelatedTaskMap" + "allOf": [ + { + "$ref": "#/definitions/models.RelatedTaskMap" + } + ] }, "reminder_dates": { "description": "An array of datetimes when the user wants to be reminded of the task.", @@ -8447,7 +8521,11 @@ }, "repeat_mode": { "description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.", - "type": "integer" + "allOf": [ + { + "$ref": "#/definitions/models.TaskRepeatMode" + } + ] }, "start_date": { "description": "When this task starts.", @@ -8455,7 +8533,11 @@ }, "subscription": { "description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.", - "$ref": "#/definitions/models.Subscription" + "allOf": [ + { + "$ref": "#/definitions/models.Subscription" + } + ] }, "title": { "description": "The task text. This is what you'll see in the list.", @@ -8465,9 +8547,7 @@ "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskAssginee": { @@ -8478,9 +8558,7 @@ }, "user_id": { "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskAttachment": { @@ -8500,9 +8578,7 @@ }, "task_id": { "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskCollection": { @@ -8550,9 +8626,7 @@ "items": { "type": "string" } - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskComment": { @@ -8572,9 +8646,7 @@ }, "updated": { "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TaskRelation": { @@ -8586,7 +8658,11 @@ }, "created_by": { "description": "The user who created this relation", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "other_task_id": { "description": "The ID of the other task, the task which is being related.", @@ -8594,16 +8670,31 @@ }, "relation_kind": { "description": "The kind of the relation.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/models.RelationKind" + } + ] }, "task_id": { "description": "The ID of the \"base\" task, the task which has a relation to another.", "type": "integer" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, + "models.TaskRepeatMode": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "TaskRepeatModeDefault", + "TaskRepeatModeMonth", + "TaskRepeatModeFromCurrentDate" + ] + }, "models.Team": { "type": "object", "properties": { @@ -8613,7 +8704,11 @@ }, "created_by": { "description": "The user who created this team.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The team's description.", @@ -8644,9 +8739,7 @@ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamList": { @@ -8662,9 +8755,13 @@ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "team_id": { "description": "The team id.", @@ -8673,9 +8770,7 @@ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamMember": { @@ -8696,9 +8791,7 @@ "username": { "description": "The username of the member. We use this to prevent automated user id entering.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamNamespace": { @@ -8714,9 +8807,13 @@ }, "right": { "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", "default": 0, - "maximum": 2 + "maximum": 2, + "allOf": [ + { + "$ref": "#/definitions/models.Right" + } + ] }, "team_id": { "description": "The team id.", @@ -8725,9 +8822,7 @@ "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.TeamUser": { @@ -8763,8 +8858,7 @@ "type": "string", "maxLength": 250, "minLength": 1 - }, - "web.Auth": {} + } } }, "models.TeamWithRight": { @@ -8776,7 +8870,11 @@ }, "created_by": { "description": "The user who created this team.", - "$ref": "#/definitions/user.User" + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] }, "description": { "description": "The team's description.", @@ -8805,16 +8903,12 @@ "maxLength": 250 }, "right": { - "description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", - "default": 0 + "$ref": "#/definitions/models.Right" }, "updated": { "description": "A timestamp when this relation was last updated. You cannot change this value.", "type": "string" - }, - "web.CRUDable": {}, - "web.Rights": {} + } } }, "models.UserWithRight": { @@ -8838,9 +8932,7 @@ "type": "string" }, "right": { - "description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.", - "type": "integer", - "default": 0 + "$ref": "#/definitions/models.Right" }, "updated": { "description": "A timestamp when this task was last updated. You cannot change this value.", @@ -8851,8 +8943,7 @@ "type": "string", "maxLength": 250, "minLength": 1 - }, - "web.Auth": {} + } } }, "notifications.DatabaseNotification": { @@ -9087,8 +9178,7 @@ "type": "string", "maxLength": 250, "minLength": 1 - }, - "web.Auth": {} + } } }, "v1.LinkShareAuth": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 31fa4f736..102d6af3b 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -58,30 +58,9 @@ definitions: value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who initially created the bucket. - filter_by: - description: The field name of the field to filter by - items: - type: string - type: array - filter_comparator: - description: The comparator for field and value - items: - type: string - type: array - filter_concat: - description: The way all filter conditions are concatenated together, can - be either "and" or "or"., - type: string - filter_include_nulls: - description: If set to true, the result will also include null values - type: boolean - filter_value: - description: The value of the field name to filter by - items: - type: string - type: array id: description: The unique, numeric id of this bucket. type: integer @@ -97,22 +76,10 @@ definitions: list_id: description: The list this bucket belongs to. type: integer - order_by: - description: The query parameter to order the items by. This can be either - asc or desc, with asc being the default. - items: - type: string - type: array position: description: The position this bucket has when querying all buckets. See the tasks.position property on how to use this. type: number - sort_by: - description: The query parameter to sort by. This is for ex. done, priority, - etc. - items: - type: string - type: array tasks: description: All tasks which belong to this bucket. items: @@ -126,8 +93,6 @@ definitions: description: A timestamp when this bucket was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.BulkAssignees: properties: @@ -136,8 +101,6 @@ definitions: items: $ref: '#/definitions/user.User' type: array - web.CRUDable: {} - web.Rights: {} type: object models.BulkTask: properties: @@ -163,7 +126,8 @@ definitions: value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who initially created the task. description: description: The task description. @@ -228,7 +192,8 @@ definitions: sort by this later. type: integer related_tasks: - $ref: '#/definitions/models.RelatedTaskMap' + allOf: + - $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: description: An array of datetimes when the user wants to be reminded of the @@ -242,16 +207,18 @@ definitions: increase all remindes and the due date by its amount. type: integer repeat_mode: + allOf: + - $ref: '#/definitions/models.TaskRepeatMode' description: 'Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.' - type: integer start_date: description: When this task starts. type: string subscription: - $ref: '#/definitions/models.Subscription' + allOf: + - $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one task. @@ -268,8 +235,6 @@ definitions: description: A timestamp when this task was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.DatabaseNotifications: properties: @@ -294,8 +259,6 @@ definitions: description: When this notification is marked as read, this will be updated with the current timestamp. type: string - web.CRUDable: {} - web.Rights: {} type: object models.Label: properties: @@ -304,7 +267,8 @@ definitions: value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who created this label description: description: The label description. @@ -326,8 +290,6 @@ definitions: description: A timestamp when this label was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.LabelTask: properties: @@ -338,8 +300,6 @@ definitions: label_id: description: The label id you want to associate with a task. type: integer - web.CRUDable: {} - web.Rights: {} type: object models.LabelTaskBulk: properties: @@ -348,8 +308,6 @@ definitions: items: $ref: '#/definitions/models.Label' type: array - web.CRUDable: {} - web.Rights: {} type: object models.LinkSharing: properties: @@ -372,26 +330,27 @@ definitions: it after the link share has been created. type: string right: + allOf: + - $ref: '#/definitions/models.Right' default: 0 description: The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 - type: integer shared_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who shared this list sharing_type: + allOf: + - $ref: '#/definitions/models.SharingType' default: 0 description: The kind of this link. 0 = undefined, 1 = without password, 2 = with password. maximum: 2 - type: integer updated: description: A timestamp when this share was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.List: properties: @@ -433,14 +392,16 @@ definitions: namespace_id: type: integer owner: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who created this list. position: description: The position this list has when querying all lists. See the tasks.position property on how to use this. type: number subscription: - $ref: '#/definitions/models.Subscription' + allOf: + - $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one list. @@ -453,19 +414,16 @@ definitions: description: A timestamp when this list was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.ListDuplicate: properties: list: - $ref: '#/definitions/models.List' + allOf: + - $ref: '#/definitions/models.List' description: The copied list namespace_id: description: The target namespace ID type: integer - web.CRUDable: {} - web.Rights: {} type: object models.ListUser: properties: @@ -477,11 +435,12 @@ definitions: description: The unique, numeric id of this list <-> user relation. type: integer right: + allOf: + - $ref: '#/definitions/models.Right' default: 0 description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 - type: integer updated: description: A timestamp when this relation was last updated. You cannot change this value. @@ -489,8 +448,6 @@ definitions: user_id: description: The username. type: string - web.CRUDable: {} - web.Rights: {} type: object models.Message: properties: @@ -518,10 +475,12 @@ definitions: description: Whether or not a namespace is archived. type: boolean owner: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who owns this namespace subscription: - $ref: '#/definitions/models.Subscription' + allOf: + - $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one namespace. @@ -534,8 +493,6 @@ definitions: description: A timestamp when this namespace was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.NamespaceUser: properties: @@ -547,11 +504,12 @@ definitions: description: The unique, numeric id of this namespace <-> user relation. type: integer right: + allOf: + - $ref: '#/definitions/models.Right' default: 0 description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 - type: integer updated: description: A timestamp when this relation was last updated. You cannot change this value. @@ -559,8 +517,6 @@ definitions: user_id: description: The username. type: string - web.CRUDable: {} - web.Rights: {} type: object models.NamespaceWithLists: properties: @@ -586,10 +542,12 @@ definitions: $ref: '#/definitions/models.List' type: array owner: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who owns this namespace subscription: - $ref: '#/definitions/models.Subscription' + allOf: + - $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one namespace. @@ -602,8 +560,6 @@ definitions: description: A timestamp when this namespace was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.RelatedTaskMap: additionalProperties: @@ -611,6 +567,44 @@ definitions: $ref: '#/definitions/models.Task' type: array type: object + models.RelationKind: + enum: + - unknown + - subtask + - parenttask + - related + - duplicateof + - duplicates + - blocking + - blocked + - precedes + - follows + - copiedfrom + - copiedto + type: string + x-enum-varnames: + - RelationKindUnknown + - RelationKindSubtask + - RelationKindParenttask + - RelationKindRelated + - RelationKindDuplicateOf + - RelationKindDuplicates + - RelationKindBlocking + - RelationKindBlocked + - RelationKindPreceeds + - RelationKindFollows + - RelationKindCopiedFrom + - RelationKindCopiedTo + models.Right: + enum: + - 0 + - 1 + - 2 + type: integer + x-enum-varnames: + - RightRead + - RightWrite + - RightAdmin models.SavedFilter: properties: created: @@ -621,7 +615,8 @@ definitions: description: The description of the filter type: string filters: - $ref: '#/definitions/models.TaskCollection' + allOf: + - $ref: '#/definitions/models.TaskCollection' description: The actual filters this filter contains id: description: The unique numeric id of this saved filter @@ -631,7 +626,8 @@ definitions: a separate namespace together with favorite lists. type: boolean owner: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who owns this filter title: description: The title of the filter. @@ -642,9 +638,17 @@ definitions: description: A timestamp when this filter was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object + models.SharingType: + enum: + - 0 + - 1 + - 2 + type: integer + x-enum-varnames: + - SharingTypeUnknown + - SharingTypeWithoutPassword + - SharingTypeWithPassword models.Subscription: properties: created: @@ -660,10 +664,9 @@ definitions: description: The numeric ID of the subscription type: integer user: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who made this subscription - web.CRUDable: {} - web.Rights: {} type: object models.Task: properties: @@ -689,7 +692,8 @@ definitions: value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who initially created the task. description: description: The task description. @@ -754,7 +758,8 @@ definitions: sort by this later. type: integer related_tasks: - $ref: '#/definitions/models.RelatedTaskMap' + allOf: + - $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: description: An array of datetimes when the user wants to be reminded of the @@ -768,16 +773,18 @@ definitions: increase all remindes and the due date by its amount. type: integer repeat_mode: + allOf: + - $ref: '#/definitions/models.TaskRepeatMode' description: 'Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.' - type: integer start_date: description: When this task starts. type: string subscription: - $ref: '#/definitions/models.Subscription' + allOf: + - $ref: '#/definitions/models.Subscription' description: |- The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it. Will only returned when retreiving one task. @@ -789,8 +796,6 @@ definitions: description: A timestamp when this task was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.TaskAssginee: properties: @@ -798,8 +803,6 @@ definitions: type: string user_id: type: integer - web.CRUDable: {} - web.Rights: {} type: object models.TaskAttachment: properties: @@ -813,8 +816,6 @@ definitions: type: integer task_id: type: integer - web.CRUDable: {} - web.Rights: {} type: object models.TaskCollection: properties: @@ -852,8 +853,6 @@ definitions: items: type: string type: array - web.CRUDable: {} - web.Rights: {} type: object models.TaskComment: properties: @@ -867,8 +866,6 @@ definitions: type: integer updated: type: string - web.CRUDable: {} - web.Rights: {} type: object models.TaskRelation: properties: @@ -877,20 +874,30 @@ definitions: value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who created this relation other_task_id: description: The ID of the other task, the task which is being related. type: integer relation_kind: + allOf: + - $ref: '#/definitions/models.RelationKind' description: The kind of the relation. - type: string task_id: description: The ID of the "base" task, the task which has a relation to another. type: integer - web.CRUDable: {} - web.Rights: {} type: object + models.TaskRepeatMode: + enum: + - 0 + - 1 + - 2 + type: integer + x-enum-varnames: + - TaskRepeatModeDefault + - TaskRepeatModeMonth + - TaskRepeatModeFromCurrentDate models.Team: properties: created: @@ -898,7 +905,8 @@ definitions: this value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who created this team. description: description: The team's description. @@ -924,8 +932,6 @@ definitions: description: A timestamp when this relation was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.TeamList: properties: @@ -937,11 +943,12 @@ definitions: description: The unique, numeric id of this list <-> team relation. type: integer right: + allOf: + - $ref: '#/definitions/models.Right' default: 0 description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 - type: integer team_id: description: The team id. type: integer @@ -949,8 +956,6 @@ definitions: description: A timestamp when this relation was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.TeamMember: properties: @@ -969,8 +974,6 @@ definitions: description: The username of the member. We use this to prevent automated user id entering. type: string - web.CRUDable: {} - web.Rights: {} type: object models.TeamNamespace: properties: @@ -982,11 +985,12 @@ definitions: description: The unique, numeric id of this namespace <-> team relation. type: integer right: + allOf: + - $ref: '#/definitions/models.Right' default: 0 description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. maximum: 2 - type: integer team_id: description: The team id. type: integer @@ -994,8 +998,6 @@ definitions: description: A timestamp when this relation was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.TeamUser: properties: @@ -1026,7 +1028,6 @@ definitions: maxLength: 250 minLength: 1 type: string - web.Auth: {} type: object models.TeamWithRight: properties: @@ -1035,7 +1036,8 @@ definitions: this value. type: string created_by: - $ref: '#/definitions/user.User' + allOf: + - $ref: '#/definitions/user.User' description: The user who created this team. description: description: The team's description. @@ -1058,16 +1060,11 @@ definitions: maxLength: 250 type: string right: - default: 0 - description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = - Admin. See the docs for more details. - type: integer + $ref: '#/definitions/models.Right' updated: description: A timestamp when this relation was last updated. You cannot change this value. type: string - web.CRUDable: {} - web.Rights: {} type: object models.UserWithRight: properties: @@ -1086,10 +1083,7 @@ definitions: description: The full name of the user. type: string right: - default: 0 - description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = - Admin. See the docs for more details. - type: integer + $ref: '#/definitions/models.Right' updated: description: A timestamp when this task was last updated. You cannot change this value. @@ -1099,7 +1093,6 @@ definitions: maxLength: 250 minLength: 1 type: string - web.Auth: {} type: object notifications.DatabaseNotification: properties: @@ -1270,7 +1263,6 @@ definitions: maxLength: 250 minLength: 1 type: string - web.Auth: {} type: object v1.LinkShareAuth: properties: From 146fa2c70a40173f2356b37d537aa046132d00f0 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 30 Mar 2023 17:15:24 +0200 Subject: [PATCH 34/38] return single team for GetTeamByOidcIDAndName --- pkg/models/teams.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 9d1586dda..a7dccc51e 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -20,7 +20,6 @@ import ( "time" "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/user" @@ -151,15 +150,16 @@ func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { // GetTeamByOidcIDAndName gets teams where oidc_id and name match parameters // For oidc team creation oidcID and Name need to be set func GetTeamByOidcIDAndName(s *xorm.Session, oidcID string, teamName string) (team Team, err error) { - exists, err := s. + err = s. Table("teams"). Where("oidc_id = ? AND name = ?", oidcID, teamName). - Get(&team) - log.Debugf("GetTeamByOidcIDAndName: %v, exists: %v", team.Name, exists) - if exists && err == nil { - return team, nil + OrderBy("name ASC"). + Limit(1). + Find(&team) + if err != nil { + return team, ErrOIDCTeamDoesNotExist{teamName, oidcID} } - return team, ErrOIDCTeamDoesNotExist{teamName, oidcID} + return team, err } func FindAllOidcTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err error) { From df9a77cc3d19ee5d64724e64a304fda3a05b53ab Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 8 May 2023 15:41:24 +0200 Subject: [PATCH 35/38] fix limit GetTeamByOidcIDAndName to get a single team --- pkg/models/teams.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index a7dccc51e..c9b24acf4 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -150,13 +150,13 @@ func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { // GetTeamByOidcIDAndName gets teams where oidc_id and name match parameters // For oidc team creation oidcID and Name need to be set func GetTeamByOidcIDAndName(s *xorm.Session, oidcID string, teamName string) (team Team, err error) { - err = s. + has, err := s. Table("teams"). Where("oidc_id = ? AND name = ?", oidcID, teamName). - OrderBy("name ASC"). + Asc("id"). Limit(1). - Find(&team) - if err != nil { + Get(&team) + if !has || err != nil { return team, ErrOIDCTeamDoesNotExist{teamName, oidcID} } return team, err From 0b52a6b5915af9a80ac8076a3d6c534c5ff6ec29 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 8 May 2023 15:42:02 +0200 Subject: [PATCH 36/38] work on instructions for openid.md --- pkg/modules/auth/openid/openid.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/modules/auth/openid/openid.md b/pkg/modules/auth/openid/openid.md index c63a89c22..f8e7a7c8b 100644 --- a/pkg/modules/auth/openid/openid.md +++ b/pkg/modules/auth/openid/openid.md @@ -1,8 +1,10 @@ # Assign teams via oidc -This PR adds the functionality to assign users to teams via oidc. +Adds the functionality to assign users to teams via oidc. Read carefully and brief your administrators to use this feature. +You need to configure your oidc provider as explained in the documentation below to make this feature work. Tested with oidc provider authentik. -To distinguish between teams created in vikunja and teams generated via oidc, an attribute for vikunja teams is introduced, which is called: *oidcID* +To distinguish between teams created in vikunja and teams generated via oidc, a string attribute for vikunja teams is introduced, which is called: *oidcID* +You should conigure your provider to send an oidcID to vikunja. ## Setup From 3fdbd53b3e3042da55032e343beba65caddfa1ed Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 8 May 2023 15:51:58 +0200 Subject: [PATCH 37/38] work on openid to just start group workflow when teamData is available --- pkg/modules/auth/openid/openid.go | 78 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 504810e81..6501ea7a0 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -200,23 +200,24 @@ func HandleCallback(c echo.Context) error { // does the oidc token contain well formed "vikunja_groups" through vikunja_scope teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, provider) - for _, err := range errs { - log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) - } + if teamData != nil { + for _, err := range errs { + log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err) + } - //find old teams for user through oidc - oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID) - if err != nil { - log.Errorf("No Oidc Teams found for user %v", err) - } - oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData) - if err != nil { - log.Errorf("Could not proceed with group routine %v", err) - } - errs = RemoveUserFromTeamsByIds(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) - log.Errorf("%v", errs) - for _, err := range errs { - log.Errorf("Found Error while signing out from teams %v", err) + //find old teams for user through oidc + oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID) + if err != nil { + log.Errorf("No Oidc Teams found for user %v", err) + } + oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData) + if err != nil { + log.Errorf("Could not proceed with group routine %v", err) + } + errs = RemoveUserFromTeamsByIds(s, u, utils.NotIn(oldOidcTeams, oidcTeams)) + for _, err := range errs { + log.Errorf("Found Error while signing out from teams %v", err) + } } err = s.Commit() if err != nil { @@ -229,29 +230,30 @@ func HandleCallback(c echo.Context) error { } func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models.OIDCTeamData) (oidcTeams []int64, err error) { - if len(teamData) > 0 { - // check if we have seen these teams before. - // find or create Teams and assign user as teammember. - teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) - if err != nil { - log.Errorf("Error verifying team for %v, got %v. Error: %v", u.Name, teams, err) - return nil, err - } - for _, team := range teams { - tm := models.TeamMember{TeamID: team.ID, UserID: u.ID, Username: u.Username} - exists, _ := tm.CheckMembership(s) - if !exists { - err = tm.Create(s, u) - if err != nil { - log.Errorf("Could not assign %v to %v. %v", u.Username, team.Name, err) - } + if len(teamData) == 0 { + return + } + // check if we have seen these teams before. + // find or create Teams and assign user as teammember. + teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u) + if err != nil { + log.Errorf("Error verifying team for %v, got %v. Error: %v", u.Name, teams, err) + return nil, err + } + for _, team := range teams { + tm := models.TeamMember{TeamID: team.ID, UserID: u.ID, Username: u.Username} + exists, _ := tm.CheckMembership(s) + if !exists { + err = tm.Create(s, u) + if err != nil { + log.Errorf("Could not assign %v to %v. %v", u.Username, team.Name, err) } - oidcTeams = append(oidcTeams, team.ID) } + oidcTeams = append(oidcTeams, team.ID) } return oidcTeams, err - } + func RemoveUserFromTeamsByIds(s *xorm.Session, u *user.User, teamIDs []int64) (errs []error) { errs = []error{} for _, teamID := range teamIDs { @@ -310,19 +312,19 @@ func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) ( } func CreateTeamWithData(s *xorm.Session, teamData models.OIDCTeamData, u *user.User) (team *models.Team, err error) { - tea := &models.Team{ + team = &models.Team{ Name: teamData.TeamName, Description: teamData.Description, OidcID: teamData.OidcID, } - err = tea.Create(s, u) - return tea, err + err = team.Create(s, u) + return team, err } // this functions creates an array of existing teams that was generated from the oidc data. func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []models.OIDCTeamData, u *user.User) (te []*models.Team, err error) { te = []*models.Team{} - // Procedure can only be successful if oidcID is set and converted to string + // Procedure can only be successful if oidcID is set for _, oidcTeam := range teamData { team, err := models.GetTeamByOidcIDAndName(s, oidcTeam.OidcID, oidcTeam.TeamName) if err != nil { From 10ec7d977d270a535288311118d5020ac8a2babc Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 8 May 2023 16:04:49 +0200 Subject: [PATCH 38/38] refactor unused function GetTeamsByName --- pkg/models/error.go | 27 ++------------------------- pkg/models/teams.go | 18 ------------------ 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/pkg/models/error.go b/pkg/models/error.go index 7efaca871..5729c0902 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1190,29 +1190,6 @@ func (err ErrTeamDoesNotHaveAccessToList) HTTPError() web.HTTPError { return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."} } -// ErrTeamsDoNotExist represents an error where Teams searched via non-unique name do not exist. -type ErrTeamsDoNotExist struct { - Name string -} - -// IsErrTeamsDoNotExist checks if an error is ErrTeamsDoNotExist. -func IsErrTeamsDoNotExist(err error) bool { - _, ok := err.(ErrTeamsDoNotExist) - return ok -} - -func (err ErrTeamsDoNotExist) Error() string { - return fmt.Sprintf("No teams with that name exist [Team Name: %v]", err.Name) -} - -// ErrCodeTeamsDoesNotExist holds the unique world-error code of this error -const ErrCodeTeamsDoNotExist = 6008 - -// HTTPError holds the http error description -func (err ErrTeamsDoNotExist) HTTPError() web.HTTPError { - return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No teams with that name exist"} -} - // ErrOIDCTeamDoesNotExist represents an error where a team with specified name and specified oidcId property does not exist type ErrOIDCTeamDoesNotExist struct { OidcID string @@ -1231,7 +1208,7 @@ func (err ErrOIDCTeamDoesNotExist) Error() string { } // ErrCodeTeamDoesNotExist holds the unique world-error code of this error -const ErrCodeOIDCTeamDoesNotExist = 6009 +const ErrCodeOIDCTeamDoesNotExist = 6008 // HTTPError holds the http error description func (err ErrOIDCTeamDoesNotExist) HTTPError() web.HTTPError { @@ -1254,7 +1231,7 @@ func (err ErrOIDCTeamsDoNotExistForUser) Error() string { } // ErrCodeTeamDoesNotExist holds the unique world-error code of this error -const ErrCodeOIDCTeamsDoNotExistForUser = 6010 +const ErrCodeOIDCTeamsDoNotExistForUser = 6009 // HTTPError holds the http error description func (err ErrOIDCTeamsDoNotExistForUser) HTTPError() web.HTTPError { diff --git a/pkg/models/teams.go b/pkg/models/teams.go index c9b24acf4..355f79779 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -129,24 +129,6 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) { return } -// GetTeamByName gets teams by name -func GetTeamsByName(s *xorm.Session, name string) (teams []*Team, err error) { - if name == "" { - return teams, ErrTeamsDoNotExist{name} - } - - var ts []*Team - - err = s. - Where("name = ?", name). - Find(&ts) - if err != nil || len(ts) == 0 { - return ts, ErrTeamsDoNotExist{name} - } - teams = ts - return teams, err -} - // GetTeamByOidcIDAndName gets teams where oidc_id and name match parameters // For oidc team creation oidcID and Name need to be set func GetTeamByOidcIDAndName(s *xorm.Session, oidcID string, teamName string) (team Team, err error) {