Fix iam pages permissions handling

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-30 18:33:18 +01:00
committed by Bryan Frimin
parent ca533ce7a7
commit e46a11001a
26 changed files with 1848 additions and 1686 deletions

View File

@@ -17,6 +17,8 @@
package connect_v1
import (
"context"
"errors"
"time"
"github.com/go-chi/chi/v5"
@@ -24,6 +26,8 @@ import (
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)
type (
@@ -67,3 +71,28 @@ func NewMux(logger *log.Logger, svc *iam.Service, cookieConfig securecookie.Conf
return r
}
func (r *Resolver) Permission(ctx context.Context, obj types.Node, action string) (bool, error) {
identity := IdentityFromContext(ctx)
err := r.iam.Authorizer.Authorize(
ctx,
iam.AuthorizeParams{
Principal: identity.ID,
Resource: obj.GetID(),
Action: action,
},
)
if err != nil {
var errInsufficientPermissions *iam.ErrInsufficientPermissions
if errors.As(err, &errInsufficientPermissions) {
return false, nil
}
r.logger.ErrorCtx(ctx, "cannot authorize", log.Error(err))
return false, gqlutils.InternalServerError(ctx)
}
return true, nil
}

View File

@@ -167,10 +167,9 @@ type Identity implements Node {
before: CursorKey
): PersonalAPIKeyConnection @goField(forceResolver: true) @isViewer
permission(action: String!, id: ID!): Boolean!
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
@isViewer
}
type MembershipProfile implements Node {
@@ -178,6 +177,10 @@ type MembershipProfile implements Node {
fullName: String!
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
type Organization implements Node {
@@ -217,6 +220,10 @@ type Organization implements Node {
): SAMLConfigurationConnection @goField(forceResolver: true)
viewerMembership: Membership @goField(forceResolver: true)
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
enum MembershipRole
@@ -237,9 +244,12 @@ type Membership implements Node {
profile: MembershipProfile @goField(forceResolver: true)
organization: Organization @goField(forceResolver: true)
role: MembershipRole!
permissions: [Permission!] @goField(forceResolver: true)
lastSession: Session @goField(forceResolver: true) @isViewer
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
type Invitation implements Node {
@@ -252,6 +262,10 @@ type Invitation implements Node {
createdAt: Datetime!
status: InvitationStatus!
organization: Organization @goField(forceResolver: true)
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
type Session implements Node {
@@ -262,6 +276,10 @@ type Session implements Node {
updatedAt: Datetime!
createdAt: Datetime!
expiresAt: Datetime!
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
type PersonalAPIKey implements Node {
@@ -272,16 +290,10 @@ type PersonalAPIKey implements Node {
createdAt: Datetime!
scopes: [TokenScope!]!
organizations: [Organization!]!
}
type Permission implements Node {
id: ID!
createdAt: Datetime!
application: Application!
accessLevel: AccessLevel!
organization: Organization!
principalType: PrincipalType!
principalId: ID!
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
type Application {
@@ -305,6 +317,10 @@ type SAMLConfiguration implements Node {
updatedAt: Datetime!
testLoginUrl: String! @goField(forceResolver: true)
attributeMappings: SAMLAttributeMappings!
permission(action: String!): Boolean!
@goField(forceResolver: true)
@session(required: PRESENT)
}
type SAMLAttributeMappings {

File diff suppressed because it is too large Load Diff

View File

@@ -175,6 +175,7 @@ type Invitation struct {
CreatedAt time.Time `json:"createdAt"`
Status coredata.InvitationStatus `json:"status"`
Organization *Organization `json:"organization,omitempty"`
Permission bool `json:"permission"`
}
func (Invitation) IsNode() {}
@@ -204,8 +205,8 @@ type Membership struct {
Profile *MembershipProfile `json:"profile,omitempty"`
Organization *Organization `json:"organization,omitempty"`
Role coredata.MembershipRole `json:"role"`
Permissions []*Permission `json:"permissions,omitempty"`
LastSession *Session `json:"lastSession,omitempty"`
Permission bool `json:"permission"`
}
func (Membership) IsNode() {}
@@ -217,10 +218,11 @@ type MembershipEdge struct {
}
type MembershipProfile struct {
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Permission bool `json:"permission"`
}
func (MembershipProfile) IsNode() {}
@@ -244,6 +246,7 @@ type Organization struct {
Invitations *InvitationConnection `json:"invitations,omitempty"`
SamlConfigurations *SAMLConfigurationConnection `json:"samlConfigurations,omitempty"`
ViewerMembership *Membership `json:"viewerMembership,omitempty"`
Permission bool `json:"permission"`
}
func (Organization) IsNode() {}
@@ -269,19 +272,6 @@ type PasswordRequired struct {
func (PasswordRequired) IsAssumeOrganizationSessionResult() {}
type Permission struct {
ID gid.GID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Application *Application `json:"application"`
AccessLevel AccessLevel `json:"accessLevel"`
Organization *Organization `json:"organization"`
PrincipalType PrincipalType `json:"principalType"`
PrincipalID gid.GID `json:"principalId"`
}
func (Permission) IsNode() {}
func (this Permission) GetID() gid.GID { return this.ID }
type PersonalAPIKey struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
@@ -290,6 +280,7 @@ type PersonalAPIKey struct {
CreatedAt time.Time `json:"createdAt"`
Scopes []TokenScope `json:"scopes"`
Organizations []*Organization `json:"organizations"`
Permission bool `json:"permission"`
}
func (PersonalAPIKey) IsNode() {}
@@ -384,6 +375,7 @@ type SAMLConfiguration struct {
UpdatedAt time.Time `json:"updatedAt"`
TestLoginURL string `json:"testLoginUrl"`
AttributeMappings *SAMLAttributeMappings `json:"attributeMappings"`
Permission bool `json:"permission"`
}
func (SAMLConfiguration) IsNode() {}
@@ -401,13 +393,14 @@ type SSOAvailability struct {
}
type Session struct {
ID gid.GID `json:"id"`
Identity *Identity `json:"identity,omitempty"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
ID gid.GID `json:"id"`
Identity *Identity `json:"identity,omitempty"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
Permission bool `json:"permission"`
}
func (Session) IsNode() {}

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
@@ -138,38 +137,8 @@ func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Ident
}
// Permission is the resolver for the permission field.
func (r *identityResolver) Permission(ctx context.Context, obj *types.Identity, action string, id gid.GID) (bool, error) {
err := r.iam.Authorizer.Authorize(
ctx,
iam.AuthorizeParams{
Principal: obj.ID,
Resource: id,
Action: action,
},
)
if err != nil {
var errInsufficientPermissions *iam.ErrInsufficientPermissions
if errors.As(err, &errInsufficientPermissions) {
graphql.AddError(
ctx,
&gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: err.Error(),
Extensions: map[string]interface{}{
"code": "UNAUTHORIZED",
},
},
)
return false, nil
}
r.logger.ErrorCtx(ctx, "cannot authorize", log.Error(err))
return false, gqlutils.InternalServerError(ctx)
}
return true, nil
func (r *identityResolver) Permission(ctx context.Context, obj *types.Identity, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// Organization is the resolver for the organization field.
@@ -189,6 +158,11 @@ func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invita
return types.NewOrganization(organization), nil
}
// Permission is the resolver for the permission field.
func (r *invitationResolver) Permission(ctx context.Context, obj *types.Invitation, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (*int, error) {
switch obj.Resolver.(type) {
@@ -269,11 +243,6 @@ func (r *membershipResolver) Organization(ctx context.Context, obj *types.Member
return types.NewOrganization(organization), nil
}
// Permissions is the resolver for the permissions field.
func (r *membershipResolver) Permissions(ctx context.Context, obj *types.Membership) ([]*types.Permission, error) {
panic("not implemented")
}
// LastSession is the resolver for the lastSession field.
func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Membership) (*types.Session, error) {
session := SessionFromContext(ctx)
@@ -295,6 +264,11 @@ func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Members
return types.NewSession(childSession), nil
}
// Permission is the resolver for the permission field.
func (r *membershipResolver) Permission(ctx context.Context, obj *types.Membership, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (*int, error) {
switch obj.Resolver.(type) {
@@ -320,6 +294,11 @@ func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *type
return nil, gqlutils.InternalServerError(ctx)
}
// Permission is the resolver for the permission field.
func (r *membershipProfileResolver) Permission(ctx context.Context, obj *types.MembershipProfile, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// SignIn is the resolver for the signIn field.
func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) {
// TODO: handle existing session to only open child session and chnage root session auth method to PASSWORD
@@ -1128,6 +1107,16 @@ func (r *organizationResolver) ViewerMembership(ctx context.Context, obj *types.
return types.NewMembership(membership), nil
}
// Permission is the resolver for the permission field.
func (r *organizationResolver) Permission(ctx context.Context, obj *types.Organization, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// Permission is the resolver for the permission field.
func (r *personalAPIKeyResolver) Permission(ctx context.Context, obj *types.PersonalAPIKey, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj *types.PersonalAPIKeyConnection) (*int, error) {
switch obj.Resolver.(type) {
@@ -1286,6 +1275,11 @@ func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types
return r.baseURL.WithPath("/api/connect/v1/saml/2.0/" + obj.ID.String()).MustString(), nil
}
// Permission is the resolver for the permission field.
func (r *sAMLConfigurationResolver) Permission(ctx context.Context, obj *types.SAMLConfiguration, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (*int, error) {
switch obj.Resolver.(type) {
@@ -1319,6 +1313,11 @@ func (r *sessionResolver) Identity(ctx context.Context, obj *types.Session) (*ty
return types.NewIdentity(identity), nil
}
// Permission is the resolver for the permission field.
func (r *sessionResolver) Permission(ctx context.Context, obj *types.Session, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.SessionConnection) (*int, error) {
switch obj.Resolver.(type) {
@@ -1355,12 +1354,20 @@ func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver {
return &membershipConnectionResolver{r}
}
// MembershipProfile returns schema.MembershipProfileResolver implementation.
func (r *Resolver) MembershipProfile() schema.MembershipProfileResolver {
return &membershipProfileResolver{r}
}
// Mutation returns schema.MutationResolver implementation.
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }
// Organization returns schema.OrganizationResolver implementation.
func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} }
// PersonalAPIKey returns schema.PersonalAPIKeyResolver implementation.
func (r *Resolver) PersonalAPIKey() schema.PersonalAPIKeyResolver { return &personalAPIKeyResolver{r} }
// PersonalAPIKeyConnection returns schema.PersonalAPIKeyConnectionResolver implementation.
func (r *Resolver) PersonalAPIKeyConnection() schema.PersonalAPIKeyConnectionResolver {
return &personalAPIKeyConnectionResolver{r}
@@ -1392,11 +1399,25 @@ type invitationResolver struct{ *Resolver }
type invitationConnectionResolver struct{ *Resolver }
type membershipResolver struct{ *Resolver }
type membershipConnectionResolver struct{ *Resolver }
type membershipProfileResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type personalAPIKeyResolver struct{ *Resolver }
type personalAPIKeyConnectionResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type sAMLConfigurationResolver struct{ *Resolver }
type sAMLConfigurationConnectionResolver struct{ *Resolver }
type sessionResolver struct{ *Resolver }
type sessionConnectionResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
/*
func (r *membershipResolver) Permissions(ctx context.Context, obj *types.Membership) ([]*types.Permission, error) {
panic("not implemented")
}
*/