1
0

feat: assign users to teams via OIDC claims (#1393)

This change adds the ability to sync teams via a custom openid claim. Vikunja will automatically create and delete teams as necessary, it will also add and remove users when they log in. These teams are fully managed by Vikunja and cannot be updated by a user.

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/1393
Resolves https://kolaente.dev/vikunja/vikunja/issues/1279
Resolves https://github.com/go-vikunja/vikunja/issues/42
Resolves https://kolaente.dev/vikunja/vikunja/issues/950
Co-authored-by: viehlieb <pf@pragma-shift.net>
Co-committed-by: viehlieb <pf@pragma-shift.net>
This commit is contained in:
viehlieb
2024-03-02 08:47:10 +00:00
committed by konrad
parent f18cde269b
commit ed4da96ab1
26 changed files with 686 additions and 32 deletions

View File

@ -55,3 +55,7 @@
team_id: 13
user_id: 10
created: 2018-12-01 15:13:12
-
team_id: 14
user_id: 10
created: 2018-12-01 15:13:12

View File

@ -28,4 +28,8 @@
created_by_id: 7
- id: 13
name: testteam13
created_by_id: 7
created_by_id: 7
- id: 14
name: testteam14
created_by_id: 7
oidc_id: 14

View File

@ -0,0 +1,43 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 <https://www.gnu.org/licenses/>.
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
},
})
}

View File

@ -1059,7 +1059,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
}
@ -1178,6 +1177,54 @@ func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToProject, Message: "This team does not have access to the project."}
}
// 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
}
// 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 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 = 6008
// 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 oidcId could be found."}
}
// 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.(ErrOIDCTeamsDoNotExistForUser)
return ok
}
func (err ErrOIDCTeamsDoNotExistForUser) Error() string {
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
// HTTPError holds the http error description
func (err ErrOIDCTeamsDoNotExistForUser) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No Teams with property oidcId could be found for User."}
}
// ====================
// User <-> Project errors
// ====================

View File

@ -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 {
@ -109,6 +108,12 @@ func (tm *TeamMember) Delete(s *xorm.Session, _ web.Auth) (err error) {
return
}
func (tm *TeamMember) MembershipExists(s *xorm.Session) (exists bool, err error) {
return s.
Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID).
Exist(&TeamMember{})
}
// 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.

View File

@ -38,6 +38,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"`
@ -91,6 +93,13 @@ type TeamUser struct {
TeamID int64 `json:"-"`
}
// OIDCTeamData is the relevant data for a team and is delivered by oidc token
type OIDCTeamData 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 {
@ -120,6 +129,34 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) {
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, oidcID string, teamName string) (*Team, error) {
team := &Team{}
has, err := s.
Table("teams").
Where("oidc_id = ? AND name = ?", oidcID, teamName).
Get(team)
if !has || err != nil {
return nil, ErrOIDCTeamDoesNotExist{teamName, oidcID}
}
return team, nil
}
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 != ? AND teams.oidc_id IS NOT NULL", "").
Cols("teams.id").
Find(&ts)
if ts == nil || err != nil {
return ts, err
}
return ts, nil
}
func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
if len(teams) == 0 {
@ -270,7 +307,6 @@ 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}
if err = tm.Create(s, doer); err != nil {
return err

View File

@ -21,21 +21,22 @@ import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"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
@ -53,16 +54,17 @@ 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"`
VikunjaGroups []map[string]interface{} `json:"vikunja_groups"`
}
func init() {
@ -96,6 +98,7 @@ func HandleCallback(c echo.Context) error {
// Check if the provider exists
providerKey := c.Param("provider")
provider, err := GetProvider(providerKey)
log.Debugf("Provider: %v", provider)
if err != nil {
log.Error(err)
return handler.HandleHTTPError(err, c)
@ -145,6 +148,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)
@ -198,16 +202,166 @@ func HandleCallback(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
// does the oidc token contain well formed "vikunja_groups" through vikunja_scope
log.Debugf("Checking for vikunja_groups in token %v", cl.VikunjaGroups)
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, provider)
if len(teamData) > 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, err := models.FindAllOidcTeamIDsForUser(s, u.ID)
if err != nil {
log.Debugf("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)
}
teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams)
err = RemoveUserFromTeamsByIds(s, u, teamIDsToLeave)
if err != nil {
log.Errorf("Found error while leaving teams %v", err)
}
errors := RemoveEmptySSOTeams(s, teamIDsToLeave)
if len(errors) > 0 {
for _, err := range errors {
log.Errorf("Found error while removing empty teams %v", 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 AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []models.OIDCTeamData) (oidcTeams []int64, err error) {
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.MembershipExists(s)
if !exists {
err = tm.Create(s, u)
if err != nil {
log.Errorf("Could not assign user %s to team %s: %v", u.Username, team.Name, err)
}
}
oidcTeams = append(oidcTeams, team.ID)
}
return oidcTeams, err
}
func RemoveEmptySSOTeams(s *xorm.Session, teamIDs []int64) (errs []error) {
for _, teamID := range teamIDs {
count, err := s.Where("team_id = ?", teamID).Count(&models.TeamMember{})
if count == 0 && err == nil {
log.Debugf("SSO team with id %v has no members. It will be deleted", teamID)
_, _err := s.Where("id = ?", teamID).Delete(&models.Team{})
if _err != nil {
errs = append(errs, _err)
}
}
}
return errs
}
func RemoveUserFromTeamsByIds(s *xorm.Session, u *user.User, teamIDs []int64) (err error) {
if len(teamIDs) < 1 {
return nil
}
log.Debugf("Removing team_member with user_id %v from team_ids %v", u.ID, teamIDs)
_, err = s.In("team_id", teamIDs).And("user_id = ?", u.ID).Delete(&models.TeamMember{})
return err
}
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
var description string
var oidcID string
_, exists := team["name"]
if exists {
name = team["name"].(string)
}
_, exists = team["description"]
if exists {
description = team["description"].(string)
}
_, exists = team["oidcID"]
if exists {
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.OIDCTeamData{TeamName: name, OidcID: oidcID, Description: description})
}
return teamData, errs
}
func CreateTeamWithData(s *xorm.Session, teamData models.OIDCTeamData, u *user.User) (team *models.Team, err error) {
team = &models.Team{
Name: teamData.TeamName,
Description: teamData.Description,
OidcID: teamData.OidcID,
}
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
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. Creating 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)
}
}
return te, err
}
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,

View File

@ -20,7 +20,9 @@ import (
"testing"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -95,4 +97,145 @@ func TestGetOrCreateUser(t *testing.T) {
"email": cl.Email,
}, false)
})
t.Run("existing user, non existing team", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
team := "new sso team"
oidcID := "47404"
cl := &claims{
Email: "other-email-address@some.service.com",
VikunjaGroups: []map[string]interface{}{
{"name": team, "oidcID": oidcID},
},
}
u, err := getOrCreateUser(s, cl, "https://some.service.com", "12345")
require.NoError(t, err)
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
for _, err := range errs {
require.NoError(t, err)
}
require.NoError(t, err)
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
db.AssertExists(t, "users", map[string]interface{}{
"id": u.ID,
"email": cl.Email,
}, false)
db.AssertExists(t, "teams", map[string]interface{}{
"id": oidcTeams,
"name": team,
}, false)
})
t.Run("existing user, assign to existing team", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
team := "testteam14"
oidcID := "14"
cl := &claims{
Email: "other-email-address@some.service.com",
VikunjaGroups: []map[string]interface{}{
{"name": team, "oidcID": oidcID},
},
}
u := &user.User{ID: 10}
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
for _, err := range errs {
require.NoError(t, err)
}
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
db.AssertExists(t, "team_members", map[string]interface{}{
"team_id": oidcTeams,
"user_id": u.ID,
}, false)
})
t.Run("existing user, remove from existing team", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
cl := &claims{
Email: "other-email-address@some.service.com",
VikunjaGroups: []map[string]interface{}{},
}
u := &user.User{ID: 10}
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
if len(errs) > 0 {
for _, err := range errs {
require.NoError(t, err)
}
}
oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID)
require.NoError(t, err)
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
require.NoError(t, err)
teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams)
require.NoError(t, err)
err = RemoveUserFromTeamsByIds(s, u, teamIDsToLeave)
require.NoError(t, err)
errs = RemoveEmptySSOTeams(s, teamIDsToLeave)
for _, err = range errs {
require.NoError(t, err)
}
errs = RemoveEmptySSOTeams(s, teamIDsToLeave)
for _, err = range errs {
require.NoError(t, err)
}
err = s.Commit()
require.NoError(t, err)
db.AssertMissing(t, "team_members", map[string]interface{}{
"team_id": oidcTeams,
"user_id": u.ID,
})
})
t.Run("existing user, remove from existing team and delete team", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
cl := &claims{
Email: "other-email-address@some.service.com",
VikunjaGroups: []map[string]interface{}{},
}
u := &user.User{ID: 10}
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
if len(errs) > 0 {
for _, err := range errs {
require.NoError(t, err)
}
}
oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID)
require.NoError(t, err)
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
require.NoError(t, err)
teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams)
require.NoError(t, err)
err = RemoveUserFromTeamsByIds(s, u, teamIDsToLeave)
require.NoError(t, err)
errs = RemoveEmptySSOTeams(s, teamIDsToLeave)
for _, err := range errs {
require.NoError(t, err)
}
err = s.Commit()
require.NoError(t, err)
db.AssertMissing(t, "teams", map[string]interface{}{
"id": oidcTeams,
})
})
}

View File

@ -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,6 +136,7 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
OriginalAuthURL: pi["authurl"].(string),
ClientSecret: pi["clientsecret"].(string),
LogoutURL: logoutURL,
Scope: scope,
}
cl, is := pi["clientid"].(int)

View File

@ -8300,6 +8300,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"
@ -8430,6 +8435,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"
},
@ -8573,6 +8583,9 @@ const docTemplate = `{
},
"name": {
"type": "string"
},
"scope": {
"type": "string"
}
}
},

View File

@ -8292,6 +8292,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"
@ -8422,6 +8427,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"
},
@ -8565,6 +8575,9 @@
},
"name": {
"type": "string"
},
"scope": {
"type": "string"
}
}
},

View File

@ -904,6 +904,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.
@ -1007,6 +1011,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:
@ -1116,6 +1124,8 @@ definitions:
type: string
name:
type: string
scope:
type: string
type: object
todoist.Migration:
properties:

View File

@ -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

View File

@ -0,0 +1,37 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 <https://www.gnu.org/licenses/>.
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
}