Fix iam pages permissions handling
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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() {}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user