From 7d1113e428bd62e09489ad759960fdeffdbc1b5f Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 4 Dec 2023 22:04:32 +0100 Subject: [PATCH 1/4] chore: add api-token data model Signed-off-by: Miguel Martinez Trivino --- .../internal/data/ent/apitoken.go | 182 +++++ .../internal/data/ent/apitoken/apitoken.go | 113 +++ .../internal/data/ent/apitoken/where.go | 372 ++++++++++ .../internal/data/ent/apitoken_create.go | 311 ++++++++ .../internal/data/ent/apitoken_delete.go | 88 +++ .../internal/data/ent/apitoken_query.go | 606 ++++++++++++++++ .../internal/data/ent/apitoken_update.go | 402 +++++++++++ app/controlplane/internal/data/ent/client.go | 161 ++++- app/controlplane/internal/data/ent/ent.go | 2 + .../internal/data/ent/hook/hook.go | 12 + .../ent/migrate/migrations/20231204210217.sql | 2 + .../data/ent/migrate/migrations/atlas.sum | 3 +- .../internal/data/ent/migrate/schema.go | 25 + .../internal/data/ent/mutation.go | 663 ++++++++++++++++++ .../internal/data/ent/predicate/predicate.go | 3 + app/controlplane/internal/data/ent/runtime.go | 11 + .../internal/data/ent/schema-viz.html | 2 +- .../internal/data/ent/schema/apitoken.go | 51 ++ app/controlplane/internal/data/ent/tx.go | 5 +- 19 files changed, 3002 insertions(+), 12 deletions(-) create mode 100644 app/controlplane/internal/data/ent/apitoken.go create mode 100644 app/controlplane/internal/data/ent/apitoken/apitoken.go create mode 100644 app/controlplane/internal/data/ent/apitoken/where.go create mode 100644 app/controlplane/internal/data/ent/apitoken_create.go create mode 100644 app/controlplane/internal/data/ent/apitoken_delete.go create mode 100644 app/controlplane/internal/data/ent/apitoken_query.go create mode 100644 app/controlplane/internal/data/ent/apitoken_update.go create mode 100644 app/controlplane/internal/data/ent/migrate/migrations/20231204210217.sql create mode 100644 app/controlplane/internal/data/ent/schema/apitoken.go diff --git a/app/controlplane/internal/data/ent/apitoken.go b/app/controlplane/internal/data/ent/apitoken.go new file mode 100644 index 000000000..254aade7e --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken.go @@ -0,0 +1,182 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/organization" + "github.com/google/uuid" +) + +// APIToken is the model entity for the APIToken schema. +type APIToken struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // Description holds the value of the "description" field. + Description string `json:"description,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt time.Time `json:"expires_at,omitempty"` + // RevokedAt holds the value of the "revoked_at" field. + RevokedAt time.Time `json:"revoked_at,omitempty"` + // OrganizationID holds the value of the "organization_id" field. + OrganizationID uuid.UUID `json:"organization_id,omitempty"` + // Edges holds the relations/edges for other nodes in the graph. + // The values are being populated by the APITokenQuery when eager-loading is set. + Edges APITokenEdges `json:"edges"` + selectValues sql.SelectValues +} + +// APITokenEdges holds the relations/edges for other nodes in the graph. +type APITokenEdges struct { + // Organization holds the value of the organization edge. + Organization *Organization `json:"organization,omitempty"` + // loadedTypes holds the information for reporting if a + // type was loaded (or requested) in eager-loading or not. + loadedTypes [1]bool +} + +// OrganizationOrErr returns the Organization value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e APITokenEdges) OrganizationOrErr() (*Organization, error) { + if e.loadedTypes[0] { + if e.Organization == nil { + // Edge was loaded but was not found. + return nil, &NotFoundError{label: organization.Label} + } + return e.Organization, nil + } + return nil, &NotLoadedError{edge: "organization"} +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*APIToken) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case apitoken.FieldDescription: + values[i] = new(sql.NullString) + case apitoken.FieldCreatedAt, apitoken.FieldExpiresAt, apitoken.FieldRevokedAt: + values[i] = new(sql.NullTime) + case apitoken.FieldID, apitoken.FieldOrganizationID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the APIToken fields. +func (at *APIToken) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case apitoken.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + at.ID = *value + } + case apitoken.FieldDescription: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field description", values[i]) + } else if value.Valid { + at.Description = value.String + } + case apitoken.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + at.CreatedAt = value.Time + } + case apitoken.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + at.ExpiresAt = value.Time + } + case apitoken.FieldRevokedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field revoked_at", values[i]) + } else if value.Valid { + at.RevokedAt = value.Time + } + case apitoken.FieldOrganizationID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field organization_id", values[i]) + } else if value != nil { + at.OrganizationID = *value + } + default: + at.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the APIToken. +// This includes values selected through modifiers, order, etc. +func (at *APIToken) Value(name string) (ent.Value, error) { + return at.selectValues.Get(name) +} + +// QueryOrganization queries the "organization" edge of the APIToken entity. +func (at *APIToken) QueryOrganization() *OrganizationQuery { + return NewAPITokenClient(at.config).QueryOrganization(at) +} + +// Update returns a builder for updating this APIToken. +// Note that you need to call APIToken.Unwrap() before calling this method if this APIToken +// was returned from a transaction, and the transaction was committed or rolled back. +func (at *APIToken) Update() *APITokenUpdateOne { + return NewAPITokenClient(at.config).UpdateOne(at) +} + +// Unwrap unwraps the APIToken entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (at *APIToken) Unwrap() *APIToken { + _tx, ok := at.config.driver.(*txDriver) + if !ok { + panic("ent: APIToken is not a transactional entity") + } + at.config.driver = _tx.drv + return at +} + +// String implements the fmt.Stringer. +func (at *APIToken) String() string { + var builder strings.Builder + builder.WriteString("APIToken(") + builder.WriteString(fmt.Sprintf("id=%v, ", at.ID)) + builder.WriteString("description=") + builder.WriteString(at.Description) + builder.WriteString(", ") + builder.WriteString("created_at=") + builder.WriteString(at.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("expires_at=") + builder.WriteString(at.ExpiresAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("revoked_at=") + builder.WriteString(at.RevokedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("organization_id=") + builder.WriteString(fmt.Sprintf("%v", at.OrganizationID)) + builder.WriteByte(')') + return builder.String() +} + +// APITokens is a parsable slice of APIToken. +type APITokens []*APIToken diff --git a/app/controlplane/internal/data/ent/apitoken/apitoken.go b/app/controlplane/internal/data/ent/apitoken/apitoken.go new file mode 100644 index 000000000..e81ff7703 --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken/apitoken.go @@ -0,0 +1,113 @@ +// Code generated by ent, DO NOT EDIT. + +package apitoken + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the apitoken type in the database. + Label = "api_token" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldDescription holds the string denoting the description field in the database. + FieldDescription = "description" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldRevokedAt holds the string denoting the revoked_at field in the database. + FieldRevokedAt = "revoked_at" + // FieldOrganizationID holds the string denoting the organization_id field in the database. + FieldOrganizationID = "organization_id" + // EdgeOrganization holds the string denoting the organization edge name in mutations. + EdgeOrganization = "organization" + // Table holds the table name of the apitoken in the database. + Table = "api_tokens" + // OrganizationTable is the table that holds the organization relation/edge. + OrganizationTable = "api_tokens" + // OrganizationInverseTable is the table name for the Organization entity. + // It exists in this package in order to avoid circular dependency with the "organization" package. + OrganizationInverseTable = "organizations" + // OrganizationColumn is the table column denoting the organization relation/edge. + OrganizationColumn = "organization_id" +) + +// Columns holds all SQL columns for apitoken fields. +var Columns = []string{ + FieldID, + FieldDescription, + FieldCreatedAt, + FieldExpiresAt, + FieldRevokedAt, + FieldOrganizationID, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the APIToken queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByDescription orders the results by the description field. +func ByDescription(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDescription, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByRevokedAt orders the results by the revoked_at field. +func ByRevokedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRevokedAt, opts...).ToFunc() +} + +// ByOrganizationID orders the results by the organization_id field. +func ByOrganizationID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldOrganizationID, opts...).ToFunc() +} + +// ByOrganizationField orders the results by organization field. +func ByOrganizationField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newOrganizationStep(), sql.OrderByField(field, opts...)) + } +} +func newOrganizationStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(OrganizationInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, OrganizationTable, OrganizationColumn), + ) +} diff --git a/app/controlplane/internal/data/ent/apitoken/where.go b/app/controlplane/internal/data/ent/apitoken/where.go new file mode 100644 index 000000000..e4219b884 --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken/where.go @@ -0,0 +1,372 @@ +// Code generated by ent, DO NOT EDIT. + +package apitoken + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldLTE(FieldID, id)) +} + +// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. +func Description(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldDescription, v)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldCreatedAt, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldExpiresAt, v)) +} + +// RevokedAt applies equality check predicate on the "revoked_at" field. It's identical to RevokedAtEQ. +func RevokedAt(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldRevokedAt, v)) +} + +// OrganizationID applies equality check predicate on the "organization_id" field. It's identical to OrganizationIDEQ. +func OrganizationID(v uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldOrganizationID, v)) +} + +// DescriptionEQ applies the EQ predicate on the "description" field. +func DescriptionEQ(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldDescription, v)) +} + +// DescriptionNEQ applies the NEQ predicate on the "description" field. +func DescriptionNEQ(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldNEQ(FieldDescription, v)) +} + +// DescriptionIn applies the In predicate on the "description" field. +func DescriptionIn(vs ...string) predicate.APIToken { + return predicate.APIToken(sql.FieldIn(FieldDescription, vs...)) +} + +// DescriptionNotIn applies the NotIn predicate on the "description" field. +func DescriptionNotIn(vs ...string) predicate.APIToken { + return predicate.APIToken(sql.FieldNotIn(FieldDescription, vs...)) +} + +// DescriptionGT applies the GT predicate on the "description" field. +func DescriptionGT(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldGT(FieldDescription, v)) +} + +// DescriptionGTE applies the GTE predicate on the "description" field. +func DescriptionGTE(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldGTE(FieldDescription, v)) +} + +// DescriptionLT applies the LT predicate on the "description" field. +func DescriptionLT(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldLT(FieldDescription, v)) +} + +// DescriptionLTE applies the LTE predicate on the "description" field. +func DescriptionLTE(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldLTE(FieldDescription, v)) +} + +// DescriptionContains applies the Contains predicate on the "description" field. +func DescriptionContains(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldContains(FieldDescription, v)) +} + +// DescriptionHasPrefix applies the HasPrefix predicate on the "description" field. +func DescriptionHasPrefix(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldHasPrefix(FieldDescription, v)) +} + +// DescriptionHasSuffix applies the HasSuffix predicate on the "description" field. +func DescriptionHasSuffix(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldHasSuffix(FieldDescription, v)) +} + +// DescriptionIsNil applies the IsNil predicate on the "description" field. +func DescriptionIsNil() predicate.APIToken { + return predicate.APIToken(sql.FieldIsNull(FieldDescription)) +} + +// DescriptionNotNil applies the NotNil predicate on the "description" field. +func DescriptionNotNil() predicate.APIToken { + return predicate.APIToken(sql.FieldNotNull(FieldDescription)) +} + +// DescriptionEqualFold applies the EqualFold predicate on the "description" field. +func DescriptionEqualFold(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldEqualFold(FieldDescription, v)) +} + +// DescriptionContainsFold applies the ContainsFold predicate on the "description" field. +func DescriptionContainsFold(v string) predicate.APIToken { + return predicate.APIToken(sql.FieldContainsFold(FieldDescription, v)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldLTE(FieldCreatedAt, v)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldLTE(FieldExpiresAt, v)) +} + +// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field. +func ExpiresAtIsNil() predicate.APIToken { + return predicate.APIToken(sql.FieldIsNull(FieldExpiresAt)) +} + +// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field. +func ExpiresAtNotNil() predicate.APIToken { + return predicate.APIToken(sql.FieldNotNull(FieldExpiresAt)) +} + +// RevokedAtEQ applies the EQ predicate on the "revoked_at" field. +func RevokedAtEQ(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldRevokedAt, v)) +} + +// RevokedAtNEQ applies the NEQ predicate on the "revoked_at" field. +func RevokedAtNEQ(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldNEQ(FieldRevokedAt, v)) +} + +// RevokedAtIn applies the In predicate on the "revoked_at" field. +func RevokedAtIn(vs ...time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldIn(FieldRevokedAt, vs...)) +} + +// RevokedAtNotIn applies the NotIn predicate on the "revoked_at" field. +func RevokedAtNotIn(vs ...time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldNotIn(FieldRevokedAt, vs...)) +} + +// RevokedAtGT applies the GT predicate on the "revoked_at" field. +func RevokedAtGT(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldGT(FieldRevokedAt, v)) +} + +// RevokedAtGTE applies the GTE predicate on the "revoked_at" field. +func RevokedAtGTE(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldGTE(FieldRevokedAt, v)) +} + +// RevokedAtLT applies the LT predicate on the "revoked_at" field. +func RevokedAtLT(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldLT(FieldRevokedAt, v)) +} + +// RevokedAtLTE applies the LTE predicate on the "revoked_at" field. +func RevokedAtLTE(v time.Time) predicate.APIToken { + return predicate.APIToken(sql.FieldLTE(FieldRevokedAt, v)) +} + +// RevokedAtIsNil applies the IsNil predicate on the "revoked_at" field. +func RevokedAtIsNil() predicate.APIToken { + return predicate.APIToken(sql.FieldIsNull(FieldRevokedAt)) +} + +// RevokedAtNotNil applies the NotNil predicate on the "revoked_at" field. +func RevokedAtNotNil() predicate.APIToken { + return predicate.APIToken(sql.FieldNotNull(FieldRevokedAt)) +} + +// OrganizationIDEQ applies the EQ predicate on the "organization_id" field. +func OrganizationIDEQ(v uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldEQ(FieldOrganizationID, v)) +} + +// OrganizationIDNEQ applies the NEQ predicate on the "organization_id" field. +func OrganizationIDNEQ(v uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldNEQ(FieldOrganizationID, v)) +} + +// OrganizationIDIn applies the In predicate on the "organization_id" field. +func OrganizationIDIn(vs ...uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldIn(FieldOrganizationID, vs...)) +} + +// OrganizationIDNotIn applies the NotIn predicate on the "organization_id" field. +func OrganizationIDNotIn(vs ...uuid.UUID) predicate.APIToken { + return predicate.APIToken(sql.FieldNotIn(FieldOrganizationID, vs...)) +} + +// HasOrganization applies the HasEdge predicate on the "organization" edge. +func HasOrganization() predicate.APIToken { + return predicate.APIToken(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, OrganizationTable, OrganizationColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasOrganizationWith applies the HasEdge predicate on the "organization" edge with a given conditions (other predicates). +func HasOrganizationWith(preds ...predicate.Organization) predicate.APIToken { + return predicate.APIToken(func(s *sql.Selector) { + step := newOrganizationStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.APIToken) predicate.APIToken { + return predicate.APIToken(func(s *sql.Selector) { + s1 := s.Clone().SetP(nil) + for _, p := range predicates { + p(s1) + } + s.Where(s1.P()) + }) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.APIToken) predicate.APIToken { + return predicate.APIToken(func(s *sql.Selector) { + s1 := s.Clone().SetP(nil) + for i, p := range predicates { + if i > 0 { + s1.Or() + } + p(s1) + } + s.Where(s1.P()) + }) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.APIToken) predicate.APIToken { + return predicate.APIToken(func(s *sql.Selector) { + p(s.Not()) + }) +} diff --git a/app/controlplane/internal/data/ent/apitoken_create.go b/app/controlplane/internal/data/ent/apitoken_create.go new file mode 100644 index 000000000..cc2cfba67 --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken_create.go @@ -0,0 +1,311 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/organization" + "github.com/google/uuid" +) + +// APITokenCreate is the builder for creating a APIToken entity. +type APITokenCreate struct { + config + mutation *APITokenMutation + hooks []Hook +} + +// SetDescription sets the "description" field. +func (atc *APITokenCreate) SetDescription(s string) *APITokenCreate { + atc.mutation.SetDescription(s) + return atc +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (atc *APITokenCreate) SetNillableDescription(s *string) *APITokenCreate { + if s != nil { + atc.SetDescription(*s) + } + return atc +} + +// SetCreatedAt sets the "created_at" field. +func (atc *APITokenCreate) SetCreatedAt(t time.Time) *APITokenCreate { + atc.mutation.SetCreatedAt(t) + return atc +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (atc *APITokenCreate) SetNillableCreatedAt(t *time.Time) *APITokenCreate { + if t != nil { + atc.SetCreatedAt(*t) + } + return atc +} + +// SetExpiresAt sets the "expires_at" field. +func (atc *APITokenCreate) SetExpiresAt(t time.Time) *APITokenCreate { + atc.mutation.SetExpiresAt(t) + return atc +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (atc *APITokenCreate) SetNillableExpiresAt(t *time.Time) *APITokenCreate { + if t != nil { + atc.SetExpiresAt(*t) + } + return atc +} + +// SetRevokedAt sets the "revoked_at" field. +func (atc *APITokenCreate) SetRevokedAt(t time.Time) *APITokenCreate { + atc.mutation.SetRevokedAt(t) + return atc +} + +// SetNillableRevokedAt sets the "revoked_at" field if the given value is not nil. +func (atc *APITokenCreate) SetNillableRevokedAt(t *time.Time) *APITokenCreate { + if t != nil { + atc.SetRevokedAt(*t) + } + return atc +} + +// SetOrganizationID sets the "organization_id" field. +func (atc *APITokenCreate) SetOrganizationID(u uuid.UUID) *APITokenCreate { + atc.mutation.SetOrganizationID(u) + return atc +} + +// SetID sets the "id" field. +func (atc *APITokenCreate) SetID(u uuid.UUID) *APITokenCreate { + atc.mutation.SetID(u) + return atc +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (atc *APITokenCreate) SetNillableID(u *uuid.UUID) *APITokenCreate { + if u != nil { + atc.SetID(*u) + } + return atc +} + +// SetOrganization sets the "organization" edge to the Organization entity. +func (atc *APITokenCreate) SetOrganization(o *Organization) *APITokenCreate { + return atc.SetOrganizationID(o.ID) +} + +// Mutation returns the APITokenMutation object of the builder. +func (atc *APITokenCreate) Mutation() *APITokenMutation { + return atc.mutation +} + +// Save creates the APIToken in the database. +func (atc *APITokenCreate) Save(ctx context.Context) (*APIToken, error) { + atc.defaults() + return withHooks(ctx, atc.sqlSave, atc.mutation, atc.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (atc *APITokenCreate) SaveX(ctx context.Context) *APIToken { + v, err := atc.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (atc *APITokenCreate) Exec(ctx context.Context) error { + _, err := atc.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (atc *APITokenCreate) ExecX(ctx context.Context) { + if err := atc.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (atc *APITokenCreate) defaults() { + if _, ok := atc.mutation.CreatedAt(); !ok { + v := apitoken.DefaultCreatedAt() + atc.mutation.SetCreatedAt(v) + } + if _, ok := atc.mutation.ID(); !ok { + v := apitoken.DefaultID() + atc.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (atc *APITokenCreate) check() error { + if _, ok := atc.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "APIToken.created_at"`)} + } + if _, ok := atc.mutation.OrganizationID(); !ok { + return &ValidationError{Name: "organization_id", err: errors.New(`ent: missing required field "APIToken.organization_id"`)} + } + if _, ok := atc.mutation.OrganizationID(); !ok { + return &ValidationError{Name: "organization", err: errors.New(`ent: missing required edge "APIToken.organization"`)} + } + return nil +} + +func (atc *APITokenCreate) sqlSave(ctx context.Context) (*APIToken, error) { + if err := atc.check(); err != nil { + return nil, err + } + _node, _spec := atc.createSpec() + if err := sqlgraph.CreateNode(ctx, atc.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + atc.mutation.id = &_node.ID + atc.mutation.done = true + return _node, nil +} + +func (atc *APITokenCreate) createSpec() (*APIToken, *sqlgraph.CreateSpec) { + var ( + _node = &APIToken{config: atc.config} + _spec = sqlgraph.NewCreateSpec(apitoken.Table, sqlgraph.NewFieldSpec(apitoken.FieldID, field.TypeUUID)) + ) + if id, ok := atc.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := atc.mutation.Description(); ok { + _spec.SetField(apitoken.FieldDescription, field.TypeString, value) + _node.Description = value + } + if value, ok := atc.mutation.CreatedAt(); ok { + _spec.SetField(apitoken.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := atc.mutation.ExpiresAt(); ok { + _spec.SetField(apitoken.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = value + } + if value, ok := atc.mutation.RevokedAt(); ok { + _spec.SetField(apitoken.FieldRevokedAt, field.TypeTime, value) + _node.RevokedAt = value + } + if nodes := atc.mutation.OrganizationIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: false, + Table: apitoken.OrganizationTable, + Columns: []string{apitoken.OrganizationColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.OrganizationID = nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } + return _node, _spec +} + +// APITokenCreateBulk is the builder for creating many APIToken entities in bulk. +type APITokenCreateBulk struct { + config + builders []*APITokenCreate +} + +// Save creates the APIToken entities in the database. +func (atcb *APITokenCreateBulk) Save(ctx context.Context) ([]*APIToken, error) { + specs := make([]*sqlgraph.CreateSpec, len(atcb.builders)) + nodes := make([]*APIToken, len(atcb.builders)) + mutators := make([]Mutator, len(atcb.builders)) + for i := range atcb.builders { + func(i int, root context.Context) { + builder := atcb.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*APITokenMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, atcb.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, atcb.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, atcb.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (atcb *APITokenCreateBulk) SaveX(ctx context.Context) []*APIToken { + v, err := atcb.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (atcb *APITokenCreateBulk) Exec(ctx context.Context) error { + _, err := atcb.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (atcb *APITokenCreateBulk) ExecX(ctx context.Context) { + if err := atcb.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/app/controlplane/internal/data/ent/apitoken_delete.go b/app/controlplane/internal/data/ent/apitoken_delete.go new file mode 100644 index 000000000..7c977aedc --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/predicate" +) + +// APITokenDelete is the builder for deleting a APIToken entity. +type APITokenDelete struct { + config + hooks []Hook + mutation *APITokenMutation +} + +// Where appends a list predicates to the APITokenDelete builder. +func (atd *APITokenDelete) Where(ps ...predicate.APIToken) *APITokenDelete { + atd.mutation.Where(ps...) + return atd +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (atd *APITokenDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, atd.sqlExec, atd.mutation, atd.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (atd *APITokenDelete) ExecX(ctx context.Context) int { + n, err := atd.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (atd *APITokenDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(apitoken.Table, sqlgraph.NewFieldSpec(apitoken.FieldID, field.TypeUUID)) + if ps := atd.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, atd.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + atd.mutation.done = true + return affected, err +} + +// APITokenDeleteOne is the builder for deleting a single APIToken entity. +type APITokenDeleteOne struct { + atd *APITokenDelete +} + +// Where appends a list predicates to the APITokenDelete builder. +func (atdo *APITokenDeleteOne) Where(ps ...predicate.APIToken) *APITokenDeleteOne { + atdo.atd.mutation.Where(ps...) + return atdo +} + +// Exec executes the deletion query. +func (atdo *APITokenDeleteOne) Exec(ctx context.Context) error { + n, err := atdo.atd.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{apitoken.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (atdo *APITokenDeleteOne) ExecX(ctx context.Context) { + if err := atdo.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/app/controlplane/internal/data/ent/apitoken_query.go b/app/controlplane/internal/data/ent/apitoken_query.go new file mode 100644 index 000000000..05274e1eb --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken_query.go @@ -0,0 +1,606 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/organization" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/predicate" + "github.com/google/uuid" +) + +// APITokenQuery is the builder for querying APIToken entities. +type APITokenQuery struct { + config + ctx *QueryContext + order []apitoken.OrderOption + inters []Interceptor + predicates []predicate.APIToken + withOrganization *OrganizationQuery + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the APITokenQuery builder. +func (atq *APITokenQuery) Where(ps ...predicate.APIToken) *APITokenQuery { + atq.predicates = append(atq.predicates, ps...) + return atq +} + +// Limit the number of records to be returned by this query. +func (atq *APITokenQuery) Limit(limit int) *APITokenQuery { + atq.ctx.Limit = &limit + return atq +} + +// Offset to start from. +func (atq *APITokenQuery) Offset(offset int) *APITokenQuery { + atq.ctx.Offset = &offset + return atq +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (atq *APITokenQuery) Unique(unique bool) *APITokenQuery { + atq.ctx.Unique = &unique + return atq +} + +// Order specifies how the records should be ordered. +func (atq *APITokenQuery) Order(o ...apitoken.OrderOption) *APITokenQuery { + atq.order = append(atq.order, o...) + return atq +} + +// QueryOrganization chains the current query on the "organization" edge. +func (atq *APITokenQuery) QueryOrganization() *OrganizationQuery { + query := (&OrganizationClient{config: atq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := atq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := atq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(apitoken.Table, apitoken.FieldID, selector), + sqlgraph.To(organization.Table, organization.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, apitoken.OrganizationTable, apitoken.OrganizationColumn), + ) + fromU = sqlgraph.SetNeighbors(atq.driver.Dialect(), step) + return fromU, nil + } + return query +} + +// First returns the first APIToken entity from the query. +// Returns a *NotFoundError when no APIToken was found. +func (atq *APITokenQuery) First(ctx context.Context) (*APIToken, error) { + nodes, err := atq.Limit(1).All(setContextOp(ctx, atq.ctx, "First")) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{apitoken.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (atq *APITokenQuery) FirstX(ctx context.Context) *APIToken { + node, err := atq.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first APIToken ID from the query. +// Returns a *NotFoundError when no APIToken ID was found. +func (atq *APITokenQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = atq.Limit(1).IDs(setContextOp(ctx, atq.ctx, "FirstID")); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{apitoken.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (atq *APITokenQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := atq.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single APIToken entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one APIToken entity is found. +// Returns a *NotFoundError when no APIToken entities are found. +func (atq *APITokenQuery) Only(ctx context.Context) (*APIToken, error) { + nodes, err := atq.Limit(2).All(setContextOp(ctx, atq.ctx, "Only")) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{apitoken.Label} + default: + return nil, &NotSingularError{apitoken.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (atq *APITokenQuery) OnlyX(ctx context.Context) *APIToken { + node, err := atq.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only APIToken ID in the query. +// Returns a *NotSingularError when more than one APIToken ID is found. +// Returns a *NotFoundError when no entities are found. +func (atq *APITokenQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = atq.Limit(2).IDs(setContextOp(ctx, atq.ctx, "OnlyID")); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{apitoken.Label} + default: + err = &NotSingularError{apitoken.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (atq *APITokenQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := atq.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of APITokens. +func (atq *APITokenQuery) All(ctx context.Context) ([]*APIToken, error) { + ctx = setContextOp(ctx, atq.ctx, "All") + if err := atq.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*APIToken, *APITokenQuery]() + return withInterceptors[[]*APIToken](ctx, atq, qr, atq.inters) +} + +// AllX is like All, but panics if an error occurs. +func (atq *APITokenQuery) AllX(ctx context.Context) []*APIToken { + nodes, err := atq.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of APIToken IDs. +func (atq *APITokenQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if atq.ctx.Unique == nil && atq.path != nil { + atq.Unique(true) + } + ctx = setContextOp(ctx, atq.ctx, "IDs") + if err = atq.Select(apitoken.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (atq *APITokenQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := atq.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (atq *APITokenQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, atq.ctx, "Count") + if err := atq.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, atq, querierCount[*APITokenQuery](), atq.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (atq *APITokenQuery) CountX(ctx context.Context) int { + count, err := atq.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (atq *APITokenQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, atq.ctx, "Exist") + switch _, err := atq.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (atq *APITokenQuery) ExistX(ctx context.Context) bool { + exist, err := atq.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the APITokenQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (atq *APITokenQuery) Clone() *APITokenQuery { + if atq == nil { + return nil + } + return &APITokenQuery{ + config: atq.config, + ctx: atq.ctx.Clone(), + order: append([]apitoken.OrderOption{}, atq.order...), + inters: append([]Interceptor{}, atq.inters...), + predicates: append([]predicate.APIToken{}, atq.predicates...), + withOrganization: atq.withOrganization.Clone(), + // clone intermediate query. + sql: atq.sql.Clone(), + path: atq.path, + } +} + +// WithOrganization tells the query-builder to eager-load the nodes that are connected to +// the "organization" edge. The optional arguments are used to configure the query builder of the edge. +func (atq *APITokenQuery) WithOrganization(opts ...func(*OrganizationQuery)) *APITokenQuery { + query := (&OrganizationClient{config: atq.config}).Query() + for _, opt := range opts { + opt(query) + } + atq.withOrganization = query + return atq +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Description string `json:"description,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.APIToken.Query(). +// GroupBy(apitoken.FieldDescription). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (atq *APITokenQuery) GroupBy(field string, fields ...string) *APITokenGroupBy { + atq.ctx.Fields = append([]string{field}, fields...) + grbuild := &APITokenGroupBy{build: atq} + grbuild.flds = &atq.ctx.Fields + grbuild.label = apitoken.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Description string `json:"description,omitempty"` +// } +// +// client.APIToken.Query(). +// Select(apitoken.FieldDescription). +// Scan(ctx, &v) +func (atq *APITokenQuery) Select(fields ...string) *APITokenSelect { + atq.ctx.Fields = append(atq.ctx.Fields, fields...) + sbuild := &APITokenSelect{APITokenQuery: atq} + sbuild.label = apitoken.Label + sbuild.flds, sbuild.scan = &atq.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a APITokenSelect configured with the given aggregations. +func (atq *APITokenQuery) Aggregate(fns ...AggregateFunc) *APITokenSelect { + return atq.Select().Aggregate(fns...) +} + +func (atq *APITokenQuery) prepareQuery(ctx context.Context) error { + for _, inter := range atq.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, atq); err != nil { + return err + } + } + } + for _, f := range atq.ctx.Fields { + if !apitoken.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if atq.path != nil { + prev, err := atq.path(ctx) + if err != nil { + return err + } + atq.sql = prev + } + return nil +} + +func (atq *APITokenQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*APIToken, error) { + var ( + nodes = []*APIToken{} + _spec = atq.querySpec() + loadedTypes = [1]bool{ + atq.withOrganization != nil, + } + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*APIToken).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &APIToken{config: atq.config} + nodes = append(nodes, node) + node.Edges.loadedTypes = loadedTypes + return node.assignValues(columns, values) + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, atq.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + if query := atq.withOrganization; query != nil { + if err := atq.loadOrganization(ctx, query, nodes, nil, + func(n *APIToken, e *Organization) { n.Edges.Organization = e }); err != nil { + return nil, err + } + } + return nodes, nil +} + +func (atq *APITokenQuery) loadOrganization(ctx context.Context, query *OrganizationQuery, nodes []*APIToken, init func(*APIToken), assign func(*APIToken, *Organization)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*APIToken) + for i := range nodes { + fk := nodes[i].OrganizationID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(organization.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "organization_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} + +func (atq *APITokenQuery) sqlCount(ctx context.Context) (int, error) { + _spec := atq.querySpec() + _spec.Node.Columns = atq.ctx.Fields + if len(atq.ctx.Fields) > 0 { + _spec.Unique = atq.ctx.Unique != nil && *atq.ctx.Unique + } + return sqlgraph.CountNodes(ctx, atq.driver, _spec) +} + +func (atq *APITokenQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(apitoken.Table, apitoken.Columns, sqlgraph.NewFieldSpec(apitoken.FieldID, field.TypeUUID)) + _spec.From = atq.sql + if unique := atq.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if atq.path != nil { + _spec.Unique = true + } + if fields := atq.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, apitoken.FieldID) + for i := range fields { + if fields[i] != apitoken.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + if atq.withOrganization != nil { + _spec.Node.AddColumnOnce(apitoken.FieldOrganizationID) + } + } + if ps := atq.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := atq.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := atq.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := atq.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (atq *APITokenQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(atq.driver.Dialect()) + t1 := builder.Table(apitoken.Table) + columns := atq.ctx.Fields + if len(columns) == 0 { + columns = apitoken.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if atq.sql != nil { + selector = atq.sql + selector.Select(selector.Columns(columns...)...) + } + if atq.ctx.Unique != nil && *atq.ctx.Unique { + selector.Distinct() + } + for _, p := range atq.predicates { + p(selector) + } + for _, p := range atq.order { + p(selector) + } + if offset := atq.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := atq.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// APITokenGroupBy is the group-by builder for APIToken entities. +type APITokenGroupBy struct { + selector + build *APITokenQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (atgb *APITokenGroupBy) Aggregate(fns ...AggregateFunc) *APITokenGroupBy { + atgb.fns = append(atgb.fns, fns...) + return atgb +} + +// Scan applies the selector query and scans the result into the given value. +func (atgb *APITokenGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, atgb.build.ctx, "GroupBy") + if err := atgb.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*APITokenQuery, *APITokenGroupBy](ctx, atgb.build, atgb, atgb.build.inters, v) +} + +func (atgb *APITokenGroupBy) sqlScan(ctx context.Context, root *APITokenQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(atgb.fns)) + for _, fn := range atgb.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*atgb.flds)+len(atgb.fns)) + for _, f := range *atgb.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*atgb.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := atgb.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// APITokenSelect is the builder for selecting fields of APIToken entities. +type APITokenSelect struct { + *APITokenQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (ats *APITokenSelect) Aggregate(fns ...AggregateFunc) *APITokenSelect { + ats.fns = append(ats.fns, fns...) + return ats +} + +// Scan applies the selector query and scans the result into the given value. +func (ats *APITokenSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, ats.ctx, "Select") + if err := ats.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*APITokenQuery, *APITokenSelect](ctx, ats.APITokenQuery, ats, ats.inters, v) +} + +func (ats *APITokenSelect) sqlScan(ctx context.Context, root *APITokenQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(ats.fns)) + for _, fn := range ats.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*ats.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := ats.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/app/controlplane/internal/data/ent/apitoken_update.go b/app/controlplane/internal/data/ent/apitoken_update.go new file mode 100644 index 000000000..34877be42 --- /dev/null +++ b/app/controlplane/internal/data/ent/apitoken_update.go @@ -0,0 +1,402 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/organization" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/predicate" + "github.com/google/uuid" +) + +// APITokenUpdate is the builder for updating APIToken entities. +type APITokenUpdate struct { + config + hooks []Hook + mutation *APITokenMutation +} + +// Where appends a list predicates to the APITokenUpdate builder. +func (atu *APITokenUpdate) Where(ps ...predicate.APIToken) *APITokenUpdate { + atu.mutation.Where(ps...) + return atu +} + +// SetDescription sets the "description" field. +func (atu *APITokenUpdate) SetDescription(s string) *APITokenUpdate { + atu.mutation.SetDescription(s) + return atu +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (atu *APITokenUpdate) SetNillableDescription(s *string) *APITokenUpdate { + if s != nil { + atu.SetDescription(*s) + } + return atu +} + +// ClearDescription clears the value of the "description" field. +func (atu *APITokenUpdate) ClearDescription() *APITokenUpdate { + atu.mutation.ClearDescription() + return atu +} + +// SetRevokedAt sets the "revoked_at" field. +func (atu *APITokenUpdate) SetRevokedAt(t time.Time) *APITokenUpdate { + atu.mutation.SetRevokedAt(t) + return atu +} + +// SetNillableRevokedAt sets the "revoked_at" field if the given value is not nil. +func (atu *APITokenUpdate) SetNillableRevokedAt(t *time.Time) *APITokenUpdate { + if t != nil { + atu.SetRevokedAt(*t) + } + return atu +} + +// ClearRevokedAt clears the value of the "revoked_at" field. +func (atu *APITokenUpdate) ClearRevokedAt() *APITokenUpdate { + atu.mutation.ClearRevokedAt() + return atu +} + +// SetOrganizationID sets the "organization_id" field. +func (atu *APITokenUpdate) SetOrganizationID(u uuid.UUID) *APITokenUpdate { + atu.mutation.SetOrganizationID(u) + return atu +} + +// SetOrganization sets the "organization" edge to the Organization entity. +func (atu *APITokenUpdate) SetOrganization(o *Organization) *APITokenUpdate { + return atu.SetOrganizationID(o.ID) +} + +// Mutation returns the APITokenMutation object of the builder. +func (atu *APITokenUpdate) Mutation() *APITokenMutation { + return atu.mutation +} + +// ClearOrganization clears the "organization" edge to the Organization entity. +func (atu *APITokenUpdate) ClearOrganization() *APITokenUpdate { + atu.mutation.ClearOrganization() + return atu +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (atu *APITokenUpdate) Save(ctx context.Context) (int, error) { + return withHooks(ctx, atu.sqlSave, atu.mutation, atu.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (atu *APITokenUpdate) SaveX(ctx context.Context) int { + affected, err := atu.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (atu *APITokenUpdate) Exec(ctx context.Context) error { + _, err := atu.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (atu *APITokenUpdate) ExecX(ctx context.Context) { + if err := atu.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (atu *APITokenUpdate) check() error { + if _, ok := atu.mutation.OrganizationID(); atu.mutation.OrganizationCleared() && !ok { + return errors.New(`ent: clearing a required unique edge "APIToken.organization"`) + } + return nil +} + +func (atu *APITokenUpdate) sqlSave(ctx context.Context) (n int, err error) { + if err := atu.check(); err != nil { + return n, err + } + _spec := sqlgraph.NewUpdateSpec(apitoken.Table, apitoken.Columns, sqlgraph.NewFieldSpec(apitoken.FieldID, field.TypeUUID)) + if ps := atu.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := atu.mutation.Description(); ok { + _spec.SetField(apitoken.FieldDescription, field.TypeString, value) + } + if atu.mutation.DescriptionCleared() { + _spec.ClearField(apitoken.FieldDescription, field.TypeString) + } + if atu.mutation.ExpiresAtCleared() { + _spec.ClearField(apitoken.FieldExpiresAt, field.TypeTime) + } + if value, ok := atu.mutation.RevokedAt(); ok { + _spec.SetField(apitoken.FieldRevokedAt, field.TypeTime, value) + } + if atu.mutation.RevokedAtCleared() { + _spec.ClearField(apitoken.FieldRevokedAt, field.TypeTime) + } + if atu.mutation.OrganizationCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: false, + Table: apitoken.OrganizationTable, + Columns: []string{apitoken.OrganizationColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := atu.mutation.OrganizationIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: false, + Table: apitoken.OrganizationTable, + Columns: []string{apitoken.OrganizationColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + if n, err = sqlgraph.UpdateNodes(ctx, atu.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{apitoken.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + atu.mutation.done = true + return n, nil +} + +// APITokenUpdateOne is the builder for updating a single APIToken entity. +type APITokenUpdateOne struct { + config + fields []string + hooks []Hook + mutation *APITokenMutation +} + +// SetDescription sets the "description" field. +func (atuo *APITokenUpdateOne) SetDescription(s string) *APITokenUpdateOne { + atuo.mutation.SetDescription(s) + return atuo +} + +// SetNillableDescription sets the "description" field if the given value is not nil. +func (atuo *APITokenUpdateOne) SetNillableDescription(s *string) *APITokenUpdateOne { + if s != nil { + atuo.SetDescription(*s) + } + return atuo +} + +// ClearDescription clears the value of the "description" field. +func (atuo *APITokenUpdateOne) ClearDescription() *APITokenUpdateOne { + atuo.mutation.ClearDescription() + return atuo +} + +// SetRevokedAt sets the "revoked_at" field. +func (atuo *APITokenUpdateOne) SetRevokedAt(t time.Time) *APITokenUpdateOne { + atuo.mutation.SetRevokedAt(t) + return atuo +} + +// SetNillableRevokedAt sets the "revoked_at" field if the given value is not nil. +func (atuo *APITokenUpdateOne) SetNillableRevokedAt(t *time.Time) *APITokenUpdateOne { + if t != nil { + atuo.SetRevokedAt(*t) + } + return atuo +} + +// ClearRevokedAt clears the value of the "revoked_at" field. +func (atuo *APITokenUpdateOne) ClearRevokedAt() *APITokenUpdateOne { + atuo.mutation.ClearRevokedAt() + return atuo +} + +// SetOrganizationID sets the "organization_id" field. +func (atuo *APITokenUpdateOne) SetOrganizationID(u uuid.UUID) *APITokenUpdateOne { + atuo.mutation.SetOrganizationID(u) + return atuo +} + +// SetOrganization sets the "organization" edge to the Organization entity. +func (atuo *APITokenUpdateOne) SetOrganization(o *Organization) *APITokenUpdateOne { + return atuo.SetOrganizationID(o.ID) +} + +// Mutation returns the APITokenMutation object of the builder. +func (atuo *APITokenUpdateOne) Mutation() *APITokenMutation { + return atuo.mutation +} + +// ClearOrganization clears the "organization" edge to the Organization entity. +func (atuo *APITokenUpdateOne) ClearOrganization() *APITokenUpdateOne { + atuo.mutation.ClearOrganization() + return atuo +} + +// Where appends a list predicates to the APITokenUpdate builder. +func (atuo *APITokenUpdateOne) Where(ps ...predicate.APIToken) *APITokenUpdateOne { + atuo.mutation.Where(ps...) + return atuo +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (atuo *APITokenUpdateOne) Select(field string, fields ...string) *APITokenUpdateOne { + atuo.fields = append([]string{field}, fields...) + return atuo +} + +// Save executes the query and returns the updated APIToken entity. +func (atuo *APITokenUpdateOne) Save(ctx context.Context) (*APIToken, error) { + return withHooks(ctx, atuo.sqlSave, atuo.mutation, atuo.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (atuo *APITokenUpdateOne) SaveX(ctx context.Context) *APIToken { + node, err := atuo.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (atuo *APITokenUpdateOne) Exec(ctx context.Context) error { + _, err := atuo.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (atuo *APITokenUpdateOne) ExecX(ctx context.Context) { + if err := atuo.Exec(ctx); err != nil { + panic(err) + } +} + +// check runs all checks and user-defined validators on the builder. +func (atuo *APITokenUpdateOne) check() error { + if _, ok := atuo.mutation.OrganizationID(); atuo.mutation.OrganizationCleared() && !ok { + return errors.New(`ent: clearing a required unique edge "APIToken.organization"`) + } + return nil +} + +func (atuo *APITokenUpdateOne) sqlSave(ctx context.Context) (_node *APIToken, err error) { + if err := atuo.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(apitoken.Table, apitoken.Columns, sqlgraph.NewFieldSpec(apitoken.FieldID, field.TypeUUID)) + id, ok := atuo.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "APIToken.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := atuo.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, apitoken.FieldID) + for _, f := range fields { + if !apitoken.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != apitoken.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := atuo.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := atuo.mutation.Description(); ok { + _spec.SetField(apitoken.FieldDescription, field.TypeString, value) + } + if atuo.mutation.DescriptionCleared() { + _spec.ClearField(apitoken.FieldDescription, field.TypeString) + } + if atuo.mutation.ExpiresAtCleared() { + _spec.ClearField(apitoken.FieldExpiresAt, field.TypeTime) + } + if value, ok := atuo.mutation.RevokedAt(); ok { + _spec.SetField(apitoken.FieldRevokedAt, field.TypeTime, value) + } + if atuo.mutation.RevokedAtCleared() { + _spec.ClearField(apitoken.FieldRevokedAt, field.TypeTime) + } + if atuo.mutation.OrganizationCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: false, + Table: apitoken.OrganizationTable, + Columns: []string{apitoken.OrganizationColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := atuo.mutation.OrganizationIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: false, + Table: apitoken.OrganizationTable, + Columns: []string{apitoken.OrganizationColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + _node = &APIToken{config: atuo.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, atuo.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{apitoken.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + atuo.mutation.done = true + return _node, nil +} diff --git a/app/controlplane/internal/data/ent/client.go b/app/controlplane/internal/data/ent/client.go index 3f8dfe147..1ed423af6 100644 --- a/app/controlplane/internal/data/ent/client.go +++ b/app/controlplane/internal/data/ent/client.go @@ -15,6 +15,7 @@ import ( "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casbackend" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/integration" @@ -36,6 +37,8 @@ type Client struct { config // Schema is the client for creating, migrating and dropping schema. Schema *migrate.Schema + // APIToken is the client for interacting with the APIToken builders. + APIToken *APITokenClient // CASBackend is the client for interacting with the CASBackend builders. CASBackend *CASBackendClient // CASMapping is the client for interacting with the CASMapping builders. @@ -77,6 +80,7 @@ func NewClient(opts ...Option) *Client { func (c *Client) init() { c.Schema = migrate.NewSchema(c.driver) + c.APIToken = NewAPITokenClient(c.config) c.CASBackend = NewCASBackendClient(c.config) c.CASMapping = NewCASMappingClient(c.config) c.Integration = NewIntegrationClient(c.config) @@ -173,6 +177,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { return &Tx{ ctx: ctx, config: cfg, + APIToken: NewAPITokenClient(cfg), CASBackend: NewCASBackendClient(cfg), CASMapping: NewCASMappingClient(cfg), Integration: NewIntegrationClient(cfg), @@ -206,6 +211,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) return &Tx{ ctx: ctx, config: cfg, + APIToken: NewAPITokenClient(cfg), CASBackend: NewCASBackendClient(cfg), CASMapping: NewCASMappingClient(cfg), Integration: NewIntegrationClient(cfg), @@ -226,7 +232,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) // Debug returns a new debug-client. It's used to get verbose logging on specific operations. // // client.Debug(). -// CASBackend. +// APIToken. // Query(). // Count(ctx) func (c *Client) Debug() *Client { @@ -249,7 +255,7 @@ func (c *Client) Close() error { // In order to add hooks to a specific client, call: `client.Node.Use(...)`. func (c *Client) Use(hooks ...Hook) { for _, n := range []interface{ Use(...Hook) }{ - c.CASBackend, c.CASMapping, c.Integration, c.IntegrationAttachment, + c.APIToken, c.CASBackend, c.CASMapping, c.Integration, c.IntegrationAttachment, c.Membership, c.OrgInvitation, c.Organization, c.Referrer, c.RobotAccount, c.User, c.Workflow, c.WorkflowContract, c.WorkflowContractVersion, c.WorkflowRun, @@ -262,7 +268,7 @@ func (c *Client) Use(hooks ...Hook) { // In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`. func (c *Client) Intercept(interceptors ...Interceptor) { for _, n := range []interface{ Intercept(...Interceptor) }{ - c.CASBackend, c.CASMapping, c.Integration, c.IntegrationAttachment, + c.APIToken, c.CASBackend, c.CASMapping, c.Integration, c.IntegrationAttachment, c.Membership, c.OrgInvitation, c.Organization, c.Referrer, c.RobotAccount, c.User, c.Workflow, c.WorkflowContract, c.WorkflowContractVersion, c.WorkflowRun, @@ -274,6 +280,8 @@ func (c *Client) Intercept(interceptors ...Interceptor) { // Mutate implements the ent.Mutator interface. func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { switch m := m.(type) { + case *APITokenMutation: + return c.APIToken.mutate(ctx, m) case *CASBackendMutation: return c.CASBackend.mutate(ctx, m) case *CASMappingMutation: @@ -307,6 +315,140 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { } } +// APITokenClient is a client for the APIToken schema. +type APITokenClient struct { + config +} + +// NewAPITokenClient returns a client for the APIToken from the given config. +func NewAPITokenClient(c config) *APITokenClient { + return &APITokenClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `apitoken.Hooks(f(g(h())))`. +func (c *APITokenClient) Use(hooks ...Hook) { + c.hooks.APIToken = append(c.hooks.APIToken, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `apitoken.Intercept(f(g(h())))`. +func (c *APITokenClient) Intercept(interceptors ...Interceptor) { + c.inters.APIToken = append(c.inters.APIToken, interceptors...) +} + +// Create returns a builder for creating a APIToken entity. +func (c *APITokenClient) Create() *APITokenCreate { + mutation := newAPITokenMutation(c.config, OpCreate) + return &APITokenCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of APIToken entities. +func (c *APITokenClient) CreateBulk(builders ...*APITokenCreate) *APITokenCreateBulk { + return &APITokenCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for APIToken. +func (c *APITokenClient) Update() *APITokenUpdate { + mutation := newAPITokenMutation(c.config, OpUpdate) + return &APITokenUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *APITokenClient) UpdateOne(at *APIToken) *APITokenUpdateOne { + mutation := newAPITokenMutation(c.config, OpUpdateOne, withAPIToken(at)) + return &APITokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *APITokenClient) UpdateOneID(id uuid.UUID) *APITokenUpdateOne { + mutation := newAPITokenMutation(c.config, OpUpdateOne, withAPITokenID(id)) + return &APITokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for APIToken. +func (c *APITokenClient) Delete() *APITokenDelete { + mutation := newAPITokenMutation(c.config, OpDelete) + return &APITokenDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *APITokenClient) DeleteOne(at *APIToken) *APITokenDeleteOne { + return c.DeleteOneID(at.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *APITokenClient) DeleteOneID(id uuid.UUID) *APITokenDeleteOne { + builder := c.Delete().Where(apitoken.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &APITokenDeleteOne{builder} +} + +// Query returns a query builder for APIToken. +func (c *APITokenClient) Query() *APITokenQuery { + return &APITokenQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeAPIToken}, + inters: c.Interceptors(), + } +} + +// Get returns a APIToken entity by its id. +func (c *APITokenClient) Get(ctx context.Context, id uuid.UUID) (*APIToken, error) { + return c.Query().Where(apitoken.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *APITokenClient) GetX(ctx context.Context, id uuid.UUID) *APIToken { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryOrganization queries the organization edge of a APIToken. +func (c *APITokenClient) QueryOrganization(at *APIToken) *OrganizationQuery { + query := (&OrganizationClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := at.ID + step := sqlgraph.NewStep( + sqlgraph.From(apitoken.Table, apitoken.FieldID, id), + sqlgraph.To(organization.Table, organization.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, apitoken.OrganizationTable, apitoken.OrganizationColumn), + ) + fromV = sqlgraph.Neighbors(at.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// Hooks returns the client hooks. +func (c *APITokenClient) Hooks() []Hook { + return c.hooks.APIToken +} + +// Interceptors returns the client interceptors. +func (c *APITokenClient) Interceptors() []Interceptor { + return c.inters.APIToken +} + +func (c *APITokenClient) mutate(ctx context.Context, m *APITokenMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&APITokenCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&APITokenUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&APITokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&APITokenDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown APIToken mutation op: %q", m.Op()) + } +} + // CASBackendClient is a client for the CASBackend schema. type CASBackendClient struct { config @@ -2570,13 +2712,14 @@ func (c *WorkflowRunClient) mutate(ctx context.Context, m *WorkflowRunMutation) // hooks and interceptors per client, for fast access. type ( hooks struct { - CASBackend, CASMapping, Integration, IntegrationAttachment, Membership, - OrgInvitation, Organization, Referrer, RobotAccount, User, Workflow, - WorkflowContract, WorkflowContractVersion, WorkflowRun []ent.Hook + APIToken, CASBackend, CASMapping, Integration, IntegrationAttachment, + Membership, OrgInvitation, Organization, Referrer, RobotAccount, User, + Workflow, WorkflowContract, WorkflowContractVersion, WorkflowRun []ent.Hook } inters struct { - CASBackend, CASMapping, Integration, IntegrationAttachment, Membership, - OrgInvitation, Organization, Referrer, RobotAccount, User, Workflow, - WorkflowContract, WorkflowContractVersion, WorkflowRun []ent.Interceptor + APIToken, CASBackend, CASMapping, Integration, IntegrationAttachment, + Membership, OrgInvitation, Organization, Referrer, RobotAccount, User, + Workflow, WorkflowContract, WorkflowContractVersion, + WorkflowRun []ent.Interceptor } ) diff --git a/app/controlplane/internal/data/ent/ent.go b/app/controlplane/internal/data/ent/ent.go index d49723451..5ed695da3 100644 --- a/app/controlplane/internal/data/ent/ent.go +++ b/app/controlplane/internal/data/ent/ent.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casbackend" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/integration" @@ -86,6 +87,7 @@ var ( func checkColumn(table, column string) error { initCheck.Do(func() { columnCheck = sql.NewColumnCheck(map[string]func(string) bool{ + apitoken.Table: apitoken.ValidColumn, casbackend.Table: casbackend.ValidColumn, casmapping.Table: casmapping.ValidColumn, integration.Table: integration.ValidColumn, diff --git a/app/controlplane/internal/data/ent/hook/hook.go b/app/controlplane/internal/data/ent/hook/hook.go index 82e6f996a..5ac8f4e4f 100644 --- a/app/controlplane/internal/data/ent/hook/hook.go +++ b/app/controlplane/internal/data/ent/hook/hook.go @@ -9,6 +9,18 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent" ) +// The APITokenFunc type is an adapter to allow the use of ordinary +// function as APIToken mutator. +type APITokenFunc func(context.Context, *ent.APITokenMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f APITokenFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.APITokenMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.APITokenMutation", m) +} + // The CASBackendFunc type is an adapter to allow the use of ordinary // function as CASBackend mutator. type CASBackendFunc func(context.Context, *ent.CASBackendMutation) (ent.Value, error) diff --git a/app/controlplane/internal/data/ent/migrate/migrations/20231204210217.sql b/app/controlplane/internal/data/ent/migrate/migrations/20231204210217.sql new file mode 100644 index 000000000..20cb957c1 --- /dev/null +++ b/app/controlplane/internal/data/ent/migrate/migrations/20231204210217.sql @@ -0,0 +1,2 @@ +-- Create "api_tokens" table +CREATE TABLE "api_tokens" ("id" uuid NOT NULL, "description" character varying NULL, "created_at" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, "expires_at" timestamptz NULL, "revoked_at" timestamptz NULL, "organization_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "api_tokens_organizations_organization" FOREIGN KEY ("organization_id") REFERENCES "organizations" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION); diff --git a/app/controlplane/internal/data/ent/migrate/migrations/atlas.sum b/app/controlplane/internal/data/ent/migrate/migrations/atlas.sum index 8ac6ff6e2..e4741a4c3 100644 --- a/app/controlplane/internal/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/internal/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:MLJVZIiHXqeAWEvFgQZmST/oiRh1Cd8pCi5pTpuizmY= +h1:STAn1hFPMq1i/v/8omDC1J73ADCFk6ZooThKhxXLROk= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -15,3 +15,4 @@ h1:MLJVZIiHXqeAWEvFgQZmST/oiRh1Cd8pCi5pTpuizmY= 20231109101843.sql h1:lSRWGmd08/RoQSBWVUNb01f9tLDDS8YwFLcXq8WjDHs= 20231114215539.sql h1:eh74G4oPOP2sEDMgEk8DcDNgZFDyvI2SXYJM75F9mQM= 20231116212408.sql h1:c3gFbTtY86D5/0AlSh4T/NkLfMWBzWi3E+McX8rS0As= +20231204210217.sql h1:7sZmEr3PAJ5jmuNRk0vxbhZ/eLKIm95r5hNJOQ4bRps= diff --git a/app/controlplane/internal/data/ent/migrate/schema.go b/app/controlplane/internal/data/ent/migrate/schema.go index 51e062ae7..4f8d1c54c 100644 --- a/app/controlplane/internal/data/ent/migrate/schema.go +++ b/app/controlplane/internal/data/ent/migrate/schema.go @@ -8,6 +8,29 @@ import ( ) var ( + // APITokensColumns holds the columns for the "api_tokens" table. + APITokensColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID, Unique: true}, + {Name: "description", Type: field.TypeString, Nullable: true}, + {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, + {Name: "expires_at", Type: field.TypeTime, Nullable: true}, + {Name: "revoked_at", Type: field.TypeTime, Nullable: true}, + {Name: "organization_id", Type: field.TypeUUID}, + } + // APITokensTable holds the schema information for the "api_tokens" table. + APITokensTable = &schema.Table{ + Name: "api_tokens", + Columns: APITokensColumns, + PrimaryKey: []*schema.Column{APITokensColumns[0]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "api_tokens_organizations_organization", + Columns: []*schema.Column{APITokensColumns[5]}, + RefColumns: []*schema.Column{OrganizationsColumns[0]}, + OnDelete: schema.NoAction, + }, + }, + } // CasBackendsColumns holds the columns for the "cas_backends" table. CasBackendsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, @@ -478,6 +501,7 @@ var ( } // Tables holds all the tables in the schema. Tables = []*schema.Table{ + APITokensTable, CasBackendsTable, CasMappingsTable, IntegrationsTable, @@ -499,6 +523,7 @@ var ( ) func init() { + APITokensTable.ForeignKeys[0].RefTable = OrganizationsTable CasBackendsTable.ForeignKeys[0].RefTable = OrganizationsTable CasMappingsTable.ForeignKeys[0].RefTable = CasBackendsTable CasMappingsTable.ForeignKeys[1].RefTable = WorkflowRunsTable diff --git a/app/controlplane/internal/data/ent/mutation.go b/app/controlplane/internal/data/ent/mutation.go index 6efa89f28..e49d81a8e 100644 --- a/app/controlplane/internal/data/ent/mutation.go +++ b/app/controlplane/internal/data/ent/mutation.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casbackend" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/integration" @@ -40,6 +41,7 @@ const ( OpUpdateOne = ent.OpUpdateOne // Node types. + TypeAPIToken = "APIToken" TypeCASBackend = "CASBackend" TypeCASMapping = "CASMapping" TypeIntegration = "Integration" @@ -56,6 +58,667 @@ const ( TypeWorkflowRun = "WorkflowRun" ) +// APITokenMutation represents an operation that mutates the APIToken nodes in the graph. +type APITokenMutation struct { + config + op Op + typ string + id *uuid.UUID + description *string + created_at *time.Time + expires_at *time.Time + revoked_at *time.Time + clearedFields map[string]struct{} + organization *uuid.UUID + clearedorganization bool + done bool + oldValue func(context.Context) (*APIToken, error) + predicates []predicate.APIToken +} + +var _ ent.Mutation = (*APITokenMutation)(nil) + +// apitokenOption allows management of the mutation configuration using functional options. +type apitokenOption func(*APITokenMutation) + +// newAPITokenMutation creates new mutation for the APIToken entity. +func newAPITokenMutation(c config, op Op, opts ...apitokenOption) *APITokenMutation { + m := &APITokenMutation{ + config: c, + op: op, + typ: TypeAPIToken, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withAPITokenID sets the ID field of the mutation. +func withAPITokenID(id uuid.UUID) apitokenOption { + return func(m *APITokenMutation) { + var ( + err error + once sync.Once + value *APIToken + ) + m.oldValue = func(ctx context.Context) (*APIToken, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().APIToken.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withAPIToken sets the old APIToken of the mutation. +func withAPIToken(node *APIToken) apitokenOption { + return func(m *APITokenMutation) { + m.oldValue = func(context.Context) (*APIToken, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m APITokenMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m APITokenMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of APIToken entities. +func (m *APITokenMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *APITokenMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *APITokenMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().APIToken.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetDescription sets the "description" field. +func (m *APITokenMutation) SetDescription(s string) { + m.description = &s +} + +// Description returns the value of the "description" field in the mutation. +func (m *APITokenMutation) Description() (r string, exists bool) { + v := m.description + if v == nil { + return + } + return *v, true +} + +// OldDescription returns the old "description" field's value of the APIToken entity. +// If the APIToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *APITokenMutation) OldDescription(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDescription is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDescription requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDescription: %w", err) + } + return oldValue.Description, nil +} + +// ClearDescription clears the value of the "description" field. +func (m *APITokenMutation) ClearDescription() { + m.description = nil + m.clearedFields[apitoken.FieldDescription] = struct{}{} +} + +// DescriptionCleared returns if the "description" field was cleared in this mutation. +func (m *APITokenMutation) DescriptionCleared() bool { + _, ok := m.clearedFields[apitoken.FieldDescription] + return ok +} + +// ResetDescription resets all changes to the "description" field. +func (m *APITokenMutation) ResetDescription() { + m.description = nil + delete(m.clearedFields, apitoken.FieldDescription) +} + +// SetCreatedAt sets the "created_at" field. +func (m *APITokenMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *APITokenMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the APIToken entity. +// If the APIToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *APITokenMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *APITokenMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetExpiresAt sets the "expires_at" field. +func (m *APITokenMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *APITokenMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the APIToken entity. +// If the APIToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *APITokenMutation) OldExpiresAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (m *APITokenMutation) ClearExpiresAt() { + m.expires_at = nil + m.clearedFields[apitoken.FieldExpiresAt] = struct{}{} +} + +// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation. +func (m *APITokenMutation) ExpiresAtCleared() bool { + _, ok := m.clearedFields[apitoken.FieldExpiresAt] + return ok +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *APITokenMutation) ResetExpiresAt() { + m.expires_at = nil + delete(m.clearedFields, apitoken.FieldExpiresAt) +} + +// SetRevokedAt sets the "revoked_at" field. +func (m *APITokenMutation) SetRevokedAt(t time.Time) { + m.revoked_at = &t +} + +// RevokedAt returns the value of the "revoked_at" field in the mutation. +func (m *APITokenMutation) RevokedAt() (r time.Time, exists bool) { + v := m.revoked_at + if v == nil { + return + } + return *v, true +} + +// OldRevokedAt returns the old "revoked_at" field's value of the APIToken entity. +// If the APIToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *APITokenMutation) OldRevokedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRevokedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRevokedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRevokedAt: %w", err) + } + return oldValue.RevokedAt, nil +} + +// ClearRevokedAt clears the value of the "revoked_at" field. +func (m *APITokenMutation) ClearRevokedAt() { + m.revoked_at = nil + m.clearedFields[apitoken.FieldRevokedAt] = struct{}{} +} + +// RevokedAtCleared returns if the "revoked_at" field was cleared in this mutation. +func (m *APITokenMutation) RevokedAtCleared() bool { + _, ok := m.clearedFields[apitoken.FieldRevokedAt] + return ok +} + +// ResetRevokedAt resets all changes to the "revoked_at" field. +func (m *APITokenMutation) ResetRevokedAt() { + m.revoked_at = nil + delete(m.clearedFields, apitoken.FieldRevokedAt) +} + +// SetOrganizationID sets the "organization_id" field. +func (m *APITokenMutation) SetOrganizationID(u uuid.UUID) { + m.organization = &u +} + +// OrganizationID returns the value of the "organization_id" field in the mutation. +func (m *APITokenMutation) OrganizationID() (r uuid.UUID, exists bool) { + v := m.organization + if v == nil { + return + } + return *v, true +} + +// OldOrganizationID returns the old "organization_id" field's value of the APIToken entity. +// If the APIToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *APITokenMutation) OldOrganizationID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOrganizationID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOrganizationID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOrganizationID: %w", err) + } + return oldValue.OrganizationID, nil +} + +// ResetOrganizationID resets all changes to the "organization_id" field. +func (m *APITokenMutation) ResetOrganizationID() { + m.organization = nil +} + +// ClearOrganization clears the "organization" edge to the Organization entity. +func (m *APITokenMutation) ClearOrganization() { + m.clearedorganization = true +} + +// OrganizationCleared reports if the "organization" edge to the Organization entity was cleared. +func (m *APITokenMutation) OrganizationCleared() bool { + return m.clearedorganization +} + +// OrganizationIDs returns the "organization" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// OrganizationID instead. It exists only for internal usage by the builders. +func (m *APITokenMutation) OrganizationIDs() (ids []uuid.UUID) { + if id := m.organization; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetOrganization resets all changes to the "organization" edge. +func (m *APITokenMutation) ResetOrganization() { + m.organization = nil + m.clearedorganization = false +} + +// Where appends a list predicates to the APITokenMutation builder. +func (m *APITokenMutation) Where(ps ...predicate.APIToken) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the APITokenMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *APITokenMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.APIToken, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *APITokenMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *APITokenMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (APIToken). +func (m *APITokenMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *APITokenMutation) Fields() []string { + fields := make([]string, 0, 5) + if m.description != nil { + fields = append(fields, apitoken.FieldDescription) + } + if m.created_at != nil { + fields = append(fields, apitoken.FieldCreatedAt) + } + if m.expires_at != nil { + fields = append(fields, apitoken.FieldExpiresAt) + } + if m.revoked_at != nil { + fields = append(fields, apitoken.FieldRevokedAt) + } + if m.organization != nil { + fields = append(fields, apitoken.FieldOrganizationID) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *APITokenMutation) Field(name string) (ent.Value, bool) { + switch name { + case apitoken.FieldDescription: + return m.Description() + case apitoken.FieldCreatedAt: + return m.CreatedAt() + case apitoken.FieldExpiresAt: + return m.ExpiresAt() + case apitoken.FieldRevokedAt: + return m.RevokedAt() + case apitoken.FieldOrganizationID: + return m.OrganizationID() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *APITokenMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case apitoken.FieldDescription: + return m.OldDescription(ctx) + case apitoken.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case apitoken.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case apitoken.FieldRevokedAt: + return m.OldRevokedAt(ctx) + case apitoken.FieldOrganizationID: + return m.OldOrganizationID(ctx) + } + return nil, fmt.Errorf("unknown APIToken field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *APITokenMutation) SetField(name string, value ent.Value) error { + switch name { + case apitoken.FieldDescription: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDescription(v) + return nil + case apitoken.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case apitoken.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case apitoken.FieldRevokedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRevokedAt(v) + return nil + case apitoken.FieldOrganizationID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOrganizationID(v) + return nil + } + return fmt.Errorf("unknown APIToken field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *APITokenMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *APITokenMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *APITokenMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown APIToken numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *APITokenMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(apitoken.FieldDescription) { + fields = append(fields, apitoken.FieldDescription) + } + if m.FieldCleared(apitoken.FieldExpiresAt) { + fields = append(fields, apitoken.FieldExpiresAt) + } + if m.FieldCleared(apitoken.FieldRevokedAt) { + fields = append(fields, apitoken.FieldRevokedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *APITokenMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *APITokenMutation) ClearField(name string) error { + switch name { + case apitoken.FieldDescription: + m.ClearDescription() + return nil + case apitoken.FieldExpiresAt: + m.ClearExpiresAt() + return nil + case apitoken.FieldRevokedAt: + m.ClearRevokedAt() + return nil + } + return fmt.Errorf("unknown APIToken nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *APITokenMutation) ResetField(name string) error { + switch name { + case apitoken.FieldDescription: + m.ResetDescription() + return nil + case apitoken.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case apitoken.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case apitoken.FieldRevokedAt: + m.ResetRevokedAt() + return nil + case apitoken.FieldOrganizationID: + m.ResetOrganizationID() + return nil + } + return fmt.Errorf("unknown APIToken field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *APITokenMutation) AddedEdges() []string { + edges := make([]string, 0, 1) + if m.organization != nil { + edges = append(edges, apitoken.EdgeOrganization) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *APITokenMutation) AddedIDs(name string) []ent.Value { + switch name { + case apitoken.EdgeOrganization: + if id := m.organization; id != nil { + return []ent.Value{*id} + } + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *APITokenMutation) RemovedEdges() []string { + edges := make([]string, 0, 1) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *APITokenMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *APITokenMutation) ClearedEdges() []string { + edges := make([]string, 0, 1) + if m.clearedorganization { + edges = append(edges, apitoken.EdgeOrganization) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *APITokenMutation) EdgeCleared(name string) bool { + switch name { + case apitoken.EdgeOrganization: + return m.clearedorganization + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *APITokenMutation) ClearEdge(name string) error { + switch name { + case apitoken.EdgeOrganization: + m.ClearOrganization() + return nil + } + return fmt.Errorf("unknown APIToken unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *APITokenMutation) ResetEdge(name string) error { + switch name { + case apitoken.EdgeOrganization: + m.ResetOrganization() + return nil + } + return fmt.Errorf("unknown APIToken edge %s", name) +} + // CASBackendMutation represents an operation that mutates the CASBackend nodes in the graph. type CASBackendMutation struct { config diff --git a/app/controlplane/internal/data/ent/predicate/predicate.go b/app/controlplane/internal/data/ent/predicate/predicate.go index 029e7a6af..f0ee54030 100644 --- a/app/controlplane/internal/data/ent/predicate/predicate.go +++ b/app/controlplane/internal/data/ent/predicate/predicate.go @@ -6,6 +6,9 @@ import ( "entgo.io/ent/dialect/sql" ) +// APIToken is the predicate function for apitoken builders. +type APIToken func(*sql.Selector) + // CASBackend is the predicate function for casbackend builders. type CASBackend func(*sql.Selector) diff --git a/app/controlplane/internal/data/ent/runtime.go b/app/controlplane/internal/data/ent/runtime.go index 3aa9d4d43..4fc7cc3b7 100644 --- a/app/controlplane/internal/data/ent/runtime.go +++ b/app/controlplane/internal/data/ent/runtime.go @@ -5,6 +5,7 @@ package ent import ( "time" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casbackend" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/integration" @@ -27,6 +28,16 @@ import ( // (default values, validators, hooks and policies) and stitches it // to their package variables. func init() { + apitokenFields := schema.APIToken{}.Fields() + _ = apitokenFields + // apitokenDescCreatedAt is the schema descriptor for created_at field. + apitokenDescCreatedAt := apitokenFields[2].Descriptor() + // apitoken.DefaultCreatedAt holds the default value on creation for the created_at field. + apitoken.DefaultCreatedAt = apitokenDescCreatedAt.Default.(func() time.Time) + // apitokenDescID is the schema descriptor for id field. + apitokenDescID := apitokenFields[0].Descriptor() + // apitoken.DefaultID holds the default value on creation for the id field. + apitoken.DefaultID = apitokenDescID.Default.(func() uuid.UUID) casbackendFields := schema.CASBackend{}.Fields() _ = casbackendFields // casbackendDescCreatedAt is the schema descriptor for created_at field. diff --git a/app/controlplane/internal/data/ent/schema-viz.html b/app/controlplane/internal/data/ent/schema-viz.html index bac7bea19..bbc6f7400 100644 --- a/app/controlplane/internal/data/ent/schema-viz.html +++ b/app/controlplane/internal/data/ent/schema-viz.html @@ -70,7 +70,7 @@ } - const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"}]}],\"edges\":[{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"WorkflowRun\",\"label\":\"workflow_run\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"RobotAccount\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"}]}"); + const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"}]}],\"edges\":[{\"from\":\"APIToken\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"WorkflowRun\",\"label\":\"workflow_run\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"RobotAccount\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"}]}"); const nodes = new vis.DataSet((entGraph.nodes || []).map(n => ({ id: n.id, diff --git a/app/controlplane/internal/data/ent/schema/apitoken.go b/app/controlplane/internal/data/ent/schema/apitoken.go new file mode 100644 index 000000000..827f9f6ca --- /dev/null +++ b/app/controlplane/internal/data/ent/schema/apitoken.go @@ -0,0 +1,51 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema/edge" + "entgo.io/ent/schema/field" + "github.com/google/uuid" +) + +type APIToken struct { + ent.Schema +} + +// Fields of the APIToken. +func (APIToken) Fields() []ent.Field { + return []ent.Field{ + // API token identifier + field.UUID("id", uuid.UUID{}).Default(uuid.New).Unique(), + // Optional description + field.String("description").Optional(), + field.Time("created_at").Default(time.Now).Immutable().Annotations(&entsql.Annotation{Default: "CURRENT_TIMESTAMP"}), + field.Time("expires_at").Optional().Immutable(), + // the token can be manually revoked + field.Time("revoked_at").Optional(), + field.UUID("organization_id", uuid.UUID{}), + } +} + +func (APIToken) Edges() []ent.Edge { + return []ent.Edge{ + edge.To("organization", Organization.Type).Unique().Required().Field("organization_id"), + } +} diff --git a/app/controlplane/internal/data/ent/tx.go b/app/controlplane/internal/data/ent/tx.go index c840bf829..6995f4ac2 100644 --- a/app/controlplane/internal/data/ent/tx.go +++ b/app/controlplane/internal/data/ent/tx.go @@ -12,6 +12,8 @@ import ( // Tx is a transactional client that is created by calling Client.Tx(). type Tx struct { config + // APIToken is the client for interacting with the APIToken builders. + APIToken *APITokenClient // CASBackend is the client for interacting with the CASBackend builders. CASBackend *CASBackendClient // CASMapping is the client for interacting with the CASMapping builders. @@ -171,6 +173,7 @@ func (tx *Tx) Client() *Client { } func (tx *Tx) init() { + tx.APIToken = NewAPITokenClient(tx.config) tx.CASBackend = NewCASBackendClient(tx.config) tx.CASMapping = NewCASMappingClient(tx.config) tx.Integration = NewIntegrationClient(tx.config) @@ -194,7 +197,7 @@ func (tx *Tx) init() { // of them in order to commit or rollback the transaction. // // If a closed transaction is embedded in one of the generated entities, and the entity -// applies a query, for example: CASBackend.QueryXXX(), the query will be executed +// applies a query, for example: APIToken.QueryXXX(), the query will be executed // through the driver which created this transaction. // // Note that txDriver is not goroutine safe. From 6fa524a1700137e01733830b3205a666978e880b Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 4 Dec 2023 23:04:26 +0100 Subject: [PATCH 2/4] chore: add api-token data model Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/biz/apitoken.go | 112 ++++++++++++++++ .../internal/biz/apitoken_integration_test.go | 120 ++++++++++++++++++ app/controlplane/internal/biz/biz.go | 1 + app/controlplane/internal/biz/membership.go | 1 + app/controlplane/internal/biz/organization.go | 14 +- .../internal/biz/testhelpers/database.go | 1 + .../internal/biz/testhelpers/wire_gen.go | 3 + app/controlplane/internal/data/apitoken.go | 64 ++++++++++ app/controlplane/internal/data/data.go | 1 + app/controlplane/internal/data/membership.go | 12 ++ 10 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 app/controlplane/internal/biz/apitoken.go create mode 100644 app/controlplane/internal/biz/apitoken_integration_test.go create mode 100644 app/controlplane/internal/data/apitoken.go diff --git a/app/controlplane/internal/biz/apitoken.go b/app/controlplane/internal/biz/apitoken.go new file mode 100644 index 000000000..d1b613b37 --- /dev/null +++ b/app/controlplane/internal/biz/apitoken.go @@ -0,0 +1,112 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package biz + +import ( + "context" + "fmt" + "time" + + "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf" + "github.com/go-kratos/kratos/v2/log" + "github.com/google/uuid" +) + +// API Token is used for unattended access to the control plane API. +type APIToken struct { + ID uuid.UUID + Description string + // This is the JWT value returned only during creation + JWT string + // Tokens are scoped to organizations + OrganizationID uuid.UUID + CreatedAt *time.Time + // When the token expires + ExpiresAt *time.Time + // When the token was manually revoked + RevokedAt *time.Time +} + +type APITokenRepo interface { + Create(ctx context.Context, description *string, expiresAt *time.Time, organizationID uuid.UUID) (*APIToken, error) +} + +type APITokenUseCase struct { + apiTokenRepo APITokenRepo + membershipRepo MembershipRepo + authConf *conf.Auth + logger *log.Helper +} + +func NewAPITokenUseCase(apiTokenRepo APITokenRepo, mRepo MembershipRepo, conf *conf.Auth, logger log.Logger) *APITokenUseCase { + return &APITokenUseCase{ + apiTokenRepo: apiTokenRepo, + membershipRepo: mRepo, + authConf: conf, + logger: log.NewHelper(logger), + } +} + +// This is the minimum duration that a token can be created for +const minDuration = 24 * time.Hour + +// expires in is a string that can be parsed by time.ParseDuration +func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expiresIn *string, userID, orgID string) (*APIToken, error) { + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + userUUID, err := uuid.Parse(userID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + // Make sure that the organization exists and that the user is a member of it + membership, err := uc.membershipRepo.FindByOrgAndUser(ctx, orgUUID, userUUID) + if err != nil { + return nil, fmt.Errorf("failed to find memberships: %w", err) + } else if membership == nil { + return nil, NewErrNotFound("organization") + } + + // If expiration is provided we store it + // we also validate that it's at least 24 hours and valid string format + var expiresAt *time.Time + if expiresIn != nil { + d, err := time.ParseDuration(*expiresIn) + if err != nil { + return nil, NewErrValidationStr("invalid expiration format, good values are 1d, 1w, 1m, 1y") + } + + if d < minDuration { + return nil, NewErrValidation(fmt.Errorf("expiration must be at least %s", minDuration)) + } + + expiresAt = new(time.Time) + *expiresAt = time.Now().Add(d) + } + + // NOTE: the expiration time is stored just for reference, it's also encoded in the JWT + // We store it since Chainloop will not have access to the JWT to check the expiration once created + token, err := uc.apiTokenRepo.Create(ctx, description, expiresAt, orgUUID) + if err != nil { + return nil, fmt.Errorf("storing token: %w", err) + } + + // TODO: generate JWT token + return token, nil +} diff --git a/app/controlplane/internal/biz/apitoken_integration_test.go b/app/controlplane/internal/biz/apitoken_integration_test.go new file mode 100644 index 000000000..60d01a8a5 --- /dev/null +++ b/app/controlplane/internal/biz/apitoken_integration_test.go @@ -0,0 +1,120 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package biz_test + +import ( + "context" + "testing" + + "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +func (s *apiTokenTestSuite) TestCreate() { + ctx := context.Background() + s.T().Run("invalid org ID", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, nil, nil, s.user.ID, "deadbeef") + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + s.Nil(token) + }) + + s.T().Run("invalid user ID", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, nil, nil, "deadbeef", s.org.ID) + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + s.Nil(token) + }) + + s.T().Run("user2 has no access to org", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, nil, nil, s.user2.ID, s.org.ID) + s.Error(err) + s.True(biz.IsNotFound(err)) + s.Nil(token) + }) + + s.T().Run("invalid expiration format expiration", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, nil, toPtrS("wrong"), s.user.ID, s.org.ID) + s.Error(err) + s.True(biz.IsErrValidation(err)) + s.ErrorContains(err, "invalid expiration format") + s.Nil(token) + }) + + s.T().Run("expiration below threshold", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, nil, toPtrS("1h"), s.user.ID, s.org.ID) + s.Error(err) + s.True(biz.IsErrValidation(err)) + s.ErrorContains(err, "expiration must be at least") + s.Nil(token) + }) + + s.T().Run("happy path without expiration nor description", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, nil, nil, s.user.ID, s.org.ID) + s.NoError(err) + s.NotNil(token.ID) + s.Equal(s.org.ID, token.OrganizationID.String()) + s.Empty(token.Description) + s.Nil(token.ExpiresAt) + s.Nil(token.RevokedAt) + }) + + s.T().Run("happy path with description and expiration", func(t *testing.T) { + token, err := s.APIToken.Create(ctx, toPtrS("tokenStr"), toPtrS("24h"), s.user.ID, s.org.ID) + s.NoError(err) + s.Equal(s.org.ID, token.OrganizationID.String()) + s.Equal("tokenStr", token.Description) + s.NotNil(token.ExpiresAt) + s.Nil(token.RevokedAt) + }) +} + +// Run the tests +func TestAPITokenUseCase(t *testing.T) { + suite.Run(t, new(apiTokenTestSuite)) +} + +// Utility struct to hold the test suite +type apiTokenTestSuite struct { + testhelpers.UseCasesEachTestSuite + org *biz.Organization + user, user2 *biz.User +} + +func (s *apiTokenTestSuite) SetupTest() { + t := s.T() + var err error + assert := assert.New(s.T()) + ctx := context.Background() + + s.TestingUseCases = testhelpers.NewTestingUseCases(t) + s.org, err = s.Organization.Create(ctx, "org1") + assert.NoError(err) + + // Create User 1 + s.user, err = s.User.FindOrCreateByEmail(ctx, "user-1@test.com") + assert.NoError(err) + // Attach org 1 + _, err = s.Membership.Create(ctx, s.org.ID, s.user.ID, true) + assert.NoError(err) + + // Create user 2 with no orgs + s.user2, err = s.User.FindOrCreateByEmail(ctx, "user-2@test.com") + assert.NoError(err) +} diff --git a/app/controlplane/internal/biz/biz.go b/app/controlplane/internal/biz/biz.go index e309fd1a1..cff256217 100644 --- a/app/controlplane/internal/biz/biz.go +++ b/app/controlplane/internal/biz/biz.go @@ -36,6 +36,7 @@ var ProviderSet = wire.NewSet( NewWorkflowRunExpirerUseCase, NewCASMappingUseCase, NewReferrerUseCase, + NewAPITokenUseCase, wire.Struct(new(NewIntegrationUseCaseOpts), "*"), wire.Struct(new(NewUserUseCaseParams), "*"), ) diff --git a/app/controlplane/internal/biz/membership.go b/app/controlplane/internal/biz/membership.go index 53e7da0f3..44455a9a8 100644 --- a/app/controlplane/internal/biz/membership.go +++ b/app/controlplane/internal/biz/membership.go @@ -36,6 +36,7 @@ type MembershipRepo interface { FindByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*Membership, error) FindByIDInUser(ctx context.Context, userID, ID uuid.UUID) (*Membership, error) + FindByOrgAndUser(ctx context.Context, orgID, userID uuid.UUID) (*Membership, error) SetCurrent(ctx context.Context, ID uuid.UUID) (*Membership, error) Create(ctx context.Context, orgID, userID uuid.UUID, current bool) (*Membership, error) Delete(ctx context.Context, ID uuid.UUID) error diff --git a/app/controlplane/internal/biz/organization.go b/app/controlplane/internal/biz/organization.go index 39e339c4f..17924ace0 100644 --- a/app/controlplane/internal/biz/organization.go +++ b/app/controlplane/internal/biz/organization.go @@ -107,20 +107,10 @@ func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgID string, } // Make sure that the organization exists and that the user is a member of it - memberships, err := uc.membershipRepo.FindByUser(ctx, userUUID) + membership, err := uc.membershipRepo.FindByOrgAndUser(ctx, orgUUID, userUUID) if err != nil { return nil, fmt.Errorf("failed to find memberships: %w", err) - } - - var found bool - for _, m := range memberships { - if m.OrganizationID == orgUUID { - found = true - break - } - } - - if !found { + } else if membership == nil { return nil, NewErrNotFound("organization") } diff --git a/app/controlplane/internal/biz/testhelpers/database.go b/app/controlplane/internal/biz/testhelpers/database.go index e10f9790f..ef6a80e5b 100644 --- a/app/controlplane/internal/biz/testhelpers/database.go +++ b/app/controlplane/internal/biz/testhelpers/database.go @@ -66,6 +66,7 @@ type TestingUseCases struct { CASMapping *biz.CASMappingUseCase OrgInvitation *biz.OrgInvitationUseCase Referrer *biz.ReferrerUseCase + APIToken *biz.APITokenUseCase // Repositories that can be used for custom crafting of use-cases Repos *TestingRepos } diff --git a/app/controlplane/internal/biz/testhelpers/wire_gen.go b/app/controlplane/internal/biz/testhelpers/wire_gen.go index 2504fcd54..78d9ed8d2 100644 --- a/app/controlplane/internal/biz/testhelpers/wire_gen.go +++ b/app/controlplane/internal/biz/testhelpers/wire_gen.go @@ -82,6 +82,8 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r cleanup() return nil, nil, err } + apiTokenRepo := data.NewAPITokenRepo(dataData, logger) + apiTokenUseCase := biz.NewAPITokenUseCase(apiTokenRepo, membershipRepo, auth, logger) testingRepos := &TestingRepos{ Membership: membershipRepo, Referrer: referrerRepo, @@ -104,6 +106,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r CASMapping: casMappingUseCase, OrgInvitation: orgInvitationUseCase, Referrer: referrerUseCase, + APIToken: apiTokenUseCase, Repos: testingRepos, } return testingUseCases, func() { diff --git a/app/controlplane/internal/data/apitoken.go b/app/controlplane/internal/data/apitoken.go new file mode 100644 index 000000000..fcf333f11 --- /dev/null +++ b/app/controlplane/internal/data/apitoken.go @@ -0,0 +1,64 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "context" + "fmt" + "time" + + "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent" + "github.com/go-kratos/kratos/v2/log" + "github.com/google/uuid" +) + +type APITokenRepo struct { + data *Data + log *log.Helper +} + +func NewAPITokenRepo(data *Data, logger log.Logger) biz.APITokenRepo { + return &APITokenRepo{ + data: data, + log: log.NewHelper(logger), + } +} + +// Persist the APIToken to the database. +func (r *APITokenRepo) Create(ctx context.Context, description *string, expiresAt *time.Time, organizationID uuid.UUID) (*biz.APIToken, error) { + token, err := r.data.db.APIToken.Create(). + SetNillableDescription(description). + SetNillableExpiresAt(expiresAt). + SetOrganizationID(organizationID). + Save(ctx) + if err != nil { + return nil, fmt.Errorf("saving APIToken: %w", err) + } + + return entAPITokenToBiz(token), nil +} + +func entAPITokenToBiz(t *ent.APIToken) *biz.APIToken { + return &biz.APIToken{ + ID: t.ID, + Description: t.Description, + CreatedAt: toTimePtr(t.CreatedAt), + ExpiresAt: toTimePtr(t.ExpiresAt), + RevokedAt: toTimePtr(t.RevokedAt), + OrganizationID: t.OrganizationID, + } +} diff --git a/app/controlplane/internal/data/data.go b/app/controlplane/internal/data/data.go index e76cd6eb5..43959b28b 100644 --- a/app/controlplane/internal/data/data.go +++ b/app/controlplane/internal/data/data.go @@ -54,6 +54,7 @@ var ProviderSet = wire.NewSet( NewMembershipRepo, NewOrgInvitation, NewReferrerRepo, + NewAPITokenRepo, ) // Data . diff --git a/app/controlplane/internal/data/membership.go b/app/controlplane/internal/data/membership.go index 02a5ac243..a9c5e2313 100644 --- a/app/controlplane/internal/data/membership.go +++ b/app/controlplane/internal/data/membership.go @@ -93,6 +93,18 @@ func (r *MembershipRepo) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*biz return result, nil } +// FindByOrgAndUser finds the membership for a given organization and user +func (r *MembershipRepo) FindByOrgAndUser(ctx context.Context, orgID, userID uuid.UUID) (*biz.Membership, error) { + m, err := orgScopedQuery(r.data.db, orgID). + QueryMemberships().Where(membership.HasUserWith(user.ID(userID))). + WithOrganization().WithUser().Only(ctx) + if err != nil && !ent.IsNotFound(err) { + return nil, err + } + + return entMembershipToBiz(m), nil +} + func (r *MembershipRepo) FindByIDInUser(ctx context.Context, userID, membershipID uuid.UUID) (*biz.Membership, error) { m, err := r.data.db.User.Query().Where(user.ID(userID)). QueryMemberships(). From 2513655e7269021bd8407f59b172a13c24510366 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Thu, 7 Dec 2023 17:52:18 +0100 Subject: [PATCH 3/4] fix: generate JTW token Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/biz/apitoken.go | 38 ++++--- .../internal/biz/apitoken_integration_test.go | 36 +++++-- .../internal/biz/testhelpers/wire_gen.go | 6 +- .../internal/biz/workflow_integration_test.go | 5 + .../internal/jwt/apitoken/apitoken.go | 93 ++++++++++++++++ .../internal/jwt/apitoken/apitoken_test.go | 100 ++++++++++++++++++ .../internal/jwt/robotaccount/robotaccount.go | 2 +- 7 files changed, 255 insertions(+), 25 deletions(-) create mode 100644 app/controlplane/internal/jwt/apitoken/apitoken.go create mode 100644 app/controlplane/internal/jwt/apitoken/apitoken_test.go diff --git a/app/controlplane/internal/biz/apitoken.go b/app/controlplane/internal/biz/apitoken.go index d1b613b37..30d3072c0 100644 --- a/app/controlplane/internal/biz/apitoken.go +++ b/app/controlplane/internal/biz/apitoken.go @@ -21,6 +21,8 @@ import ( "time" "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/apitoken" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" ) @@ -47,24 +49,34 @@ type APITokenRepo interface { type APITokenUseCase struct { apiTokenRepo APITokenRepo membershipRepo MembershipRepo - authConf *conf.Auth logger *log.Helper + jwtBuilder *apitoken.Builder } -func NewAPITokenUseCase(apiTokenRepo APITokenRepo, mRepo MembershipRepo, conf *conf.Auth, logger log.Logger) *APITokenUseCase { - return &APITokenUseCase{ +func NewAPITokenUseCase(apiTokenRepo APITokenRepo, mRepo MembershipRepo, conf *conf.Auth, logger log.Logger) (*APITokenUseCase, error) { + uc := &APITokenUseCase{ apiTokenRepo: apiTokenRepo, membershipRepo: mRepo, - authConf: conf, logger: log.NewHelper(logger), } + + b, err := apitoken.NewBuilder( + apitoken.WithIssuer(jwt.DefaultIssuer), + apitoken.WithKeySecret(conf.GeneratedJwsHmacSecret), + ) + if err != nil { + return nil, fmt.Errorf("creating jwt builder: %w", err) + } + + uc.jwtBuilder = b + return uc, nil } // This is the minimum duration that a token can be created for const minDuration = 24 * time.Hour // expires in is a string that can be parsed by time.ParseDuration -func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expiresIn *string, userID, orgID string) (*APIToken, error) { +func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expiresIn *time.Duration, userID, orgID string) (*APIToken, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) @@ -87,17 +99,12 @@ func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expi // we also validate that it's at least 24 hours and valid string format var expiresAt *time.Time if expiresIn != nil { - d, err := time.ParseDuration(*expiresIn) - if err != nil { - return nil, NewErrValidationStr("invalid expiration format, good values are 1d, 1w, 1m, 1y") - } - - if d < minDuration { + if *expiresIn < minDuration { return nil, NewErrValidation(fmt.Errorf("expiration must be at least %s", minDuration)) } expiresAt = new(time.Time) - *expiresAt = time.Now().Add(d) + *expiresAt = time.Now().Add(*expiresIn) } // NOTE: the expiration time is stored just for reference, it's also encoded in the JWT @@ -107,6 +114,11 @@ func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expi return nil, fmt.Errorf("storing token: %w", err) } - // TODO: generate JWT token + // generate the JWT + token.JWT, err = uc.jwtBuilder.GenerateJWT(orgID, token.ID.String(), expiresAt) + if err != nil { + return nil, fmt.Errorf("generating jwt: %w", err) + } + return token, nil } diff --git a/app/controlplane/internal/biz/apitoken_integration_test.go b/app/controlplane/internal/biz/apitoken_integration_test.go index 60d01a8a5..8257ff0f3 100644 --- a/app/controlplane/internal/biz/apitoken_integration_test.go +++ b/app/controlplane/internal/biz/apitoken_integration_test.go @@ -18,11 +18,15 @@ package biz_test import ( "context" "testing" + "time" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/apitoken" + "github.com/golang-jwt/jwt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -49,16 +53,8 @@ func (s *apiTokenTestSuite) TestCreate() { s.Nil(token) }) - s.T().Run("invalid expiration format expiration", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, toPtrS("wrong"), s.user.ID, s.org.ID) - s.Error(err) - s.True(biz.IsErrValidation(err)) - s.ErrorContains(err, "invalid expiration format") - s.Nil(token) - }) - s.T().Run("expiration below threshold", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, toPtrS("1h"), s.user.ID, s.org.ID) + token, err := s.APIToken.Create(ctx, nil, toPtrDuration(2*time.Hour), s.user.ID, s.org.ID) s.Error(err) s.True(biz.IsErrValidation(err)) s.ErrorContains(err, "expiration must be at least") @@ -73,10 +69,11 @@ func (s *apiTokenTestSuite) TestCreate() { s.Empty(token.Description) s.Nil(token.ExpiresAt) s.Nil(token.RevokedAt) + s.NotNil(token.JWT) }) s.T().Run("happy path with description and expiration", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, toPtrS("tokenStr"), toPtrS("24h"), s.user.ID, s.org.ID) + token, err := s.APIToken.Create(ctx, toPtrS("tokenStr"), toPtrDuration(24*time.Hour), s.user.ID, s.org.ID) s.NoError(err) s.Equal(s.org.ID, token.OrganizationID.String()) s.Equal("tokenStr", token.Description) @@ -85,6 +82,25 @@ func (s *apiTokenTestSuite) TestCreate() { }) } +func (s *apiTokenTestSuite) TestGeneratedJWT() { + token, err := s.APIToken.Create(context.Background(), nil, toPtrDuration(24*time.Hour), s.user.ID, s.org.ID) + s.NoError(err) + require.NotNil(s.T(), token) + + claims := &apitoken.CustomClaims{} + tokenInfo, err := jwt.ParseWithClaims(token.JWT, claims, func(_ *jwt.Token) (interface{}, error) { + return []byte("test"), nil + }) + + require.NoError(s.T(), err) + s.True(tokenInfo.Valid) + // The resulting JWT should have the same org, token ID and expiration time than + // the reference in the DB + s.Equal(token.OrganizationID.String(), claims.OrgID) + s.Equal(token.ID.String(), claims.ID) + s.Equal(token.ExpiresAt.Truncate(time.Second), claims.ExpiresAt.Truncate(time.Second)) +} + // Run the tests func TestAPITokenUseCase(t *testing.T) { suite.Run(t, new(apiTokenTestSuite)) diff --git a/app/controlplane/internal/biz/testhelpers/wire_gen.go b/app/controlplane/internal/biz/testhelpers/wire_gen.go index 78d9ed8d2..71abb944c 100644 --- a/app/controlplane/internal/biz/testhelpers/wire_gen.go +++ b/app/controlplane/internal/biz/testhelpers/wire_gen.go @@ -83,7 +83,11 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r return nil, nil, err } apiTokenRepo := data.NewAPITokenRepo(dataData, logger) - apiTokenUseCase := biz.NewAPITokenUseCase(apiTokenRepo, membershipRepo, auth, logger) + apiTokenUseCase, err := biz.NewAPITokenUseCase(apiTokenRepo, membershipRepo, auth, logger) + if err != nil { + cleanup() + return nil, nil, err + } testingRepos := &TestingRepos{ Membership: membershipRepo, Referrer: referrerRepo, diff --git a/app/controlplane/internal/biz/workflow_integration_test.go b/app/controlplane/internal/biz/workflow_integration_test.go index fcf8f76e6..bbb1d8563 100644 --- a/app/controlplane/internal/biz/workflow_integration_test.go +++ b/app/controlplane/internal/biz/workflow_integration_test.go @@ -18,6 +18,7 @@ package biz_test import ( "context" "testing" + "time" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers" @@ -158,3 +159,7 @@ func toPtrS(s string) *string { func toPtrBool(b bool) *bool { return &b } + +func toPtrDuration(d time.Duration) *time.Duration { + return &d +} diff --git a/app/controlplane/internal/jwt/apitoken/apitoken.go b/app/controlplane/internal/jwt/apitoken/apitoken.go new file mode 100644 index 000000000..2bad971aa --- /dev/null +++ b/app/controlplane/internal/jwt/apitoken/apitoken.go @@ -0,0 +1,93 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apitoken + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +var SigningMethod = jwt.SigningMethodHS256 + +const Audience = "api-token-auth.chainloop" + +type Builder struct { + issuer string + hmacSecret string +} + +type NewOpt func(b *Builder) + +func WithIssuer(issuer string) NewOpt { + return func(b *Builder) { + b.issuer = issuer + } +} + +func WithKeySecret(hmacSecret string) NewOpt { + return func(b *Builder) { + b.hmacSecret = hmacSecret + } +} + +// NewBuilder creates a new APIToken JWT builder +// It supports expiration and revocation +// Currently we use a simple hmac encryption method meant to be continuously rotated +// TODO: additional/alternative encryption method, i.e DSE asymmetric, see CAS robot account for reference +func NewBuilder(opts ...NewOpt) (*Builder, error) { + b := &Builder{} + for _, opt := range opts { + opt(b) + } + + if b.issuer == "" { + return nil, errors.New("issuer is required") + } + + if b.hmacSecret == "" { + return nil, errors.New("hmac secret is required") + } + + return b, nil +} + +// it can both expire and being revoked, revocation is performed by checking the keyID against our revocation list +func (ra *Builder) GenerateJWT(orgID, keyID string, expiresAt *time.Time) (string, error) { + claims := CustomClaims{ + orgID, + jwt.RegisteredClaims{ + // Key identifier so we can check it's revocation status + ID: keyID, + Issuer: ra.issuer, + Audience: jwt.ClaimStrings{Audience}, + }, + } + + // optional expiration value, i.e 30 days + if expiresAt != nil { + claims.ExpiresAt = jwt.NewNumericDate(*expiresAt) + } + + resultToken := jwt.NewWithClaims(SigningMethod, claims) + return resultToken.SignedString([]byte(ra.hmacSecret)) +} + +type CustomClaims struct { + OrgID string `json:"org_id"` + jwt.RegisteredClaims +} diff --git a/app/controlplane/internal/jwt/apitoken/apitoken_test.go b/app/controlplane/internal/jwt/apitoken/apitoken_test.go new file mode 100644 index 000000000..ab58d52c9 --- /dev/null +++ b/app/controlplane/internal/jwt/apitoken/apitoken_test.go @@ -0,0 +1,100 @@ +// +// Copyright 2023 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apitoken + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBuilder(t *testing.T) { + testCases := []struct { + name string + issuer string + encryptionString string + wantError bool + }{ + {"valid", "issuer", "my-key", false}, + {"invalid passphrase", "issuer", "", true}, + {"missing issuer", "", "passphrase", true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert := assert.New(t) + opts := make([]NewOpt, 0) + if tc.issuer != "" { + opts = append(opts, WithIssuer(tc.issuer)) + } + + if tc.encryptionString != "" { + opts = append(opts, WithKeySecret(tc.encryptionString)) + } + + b, err := NewBuilder(opts...) + if tc.wantError { + assert.Error(err) + return + } + + assert.NoError(err) + + if tc.issuer != "" { + assert.Equal(tc.issuer, b.issuer) + } + + if tc.encryptionString != "" { + assert.Equal(b.hmacSecret, tc.encryptionString) + } + }) + } +} + +func TestGenerateJWT(t *testing.T) { + const hmacSecret = "my-secret" + + b, err := NewBuilder( + WithIssuer("my-issuer"), + WithKeySecret(hmacSecret), + ) + require.NoError(t, err) + + token, err := b.GenerateJWT("org-id", "key-id", toPtrTime(time.Now().Add(1*time.Hour))) + assert.NoError(t, err) + assert.NotEmpty(t, token) + + // Verify signature and check claims + claims := &CustomClaims{} + tokenInfo, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { + return []byte(hmacSecret), nil + }) + + require.NoError(t, err) + assert.True(t, tokenInfo.Valid) + assert.Equal(t, "org-id", claims.OrgID) + assert.Equal(t, "key-id", claims.ID) + assert.Equal(t, "my-issuer", claims.Issuer) + assert.Contains(t, claims.Audience, Audience) + assert.NotNil(t, claims.ExpiresAt) +} + +func toPtrTime(t time.Time) *time.Time { + return &t +} diff --git a/app/controlplane/internal/jwt/robotaccount/robotaccount.go b/app/controlplane/internal/jwt/robotaccount/robotaccount.go index 49a82d372..3cd798a1e 100644 --- a/app/controlplane/internal/jwt/robotaccount/robotaccount.go +++ b/app/controlplane/internal/jwt/robotaccount/robotaccount.go @@ -45,7 +45,7 @@ func WithKeySecret(hmacSecret string) NewOpt { // NewBuilder creates a new robot account builder meant to be associated with a workflowRun // It does not expire but its revocation status is checked on every request // Currently we use a simple hmac encryption method meant to be continuously rotated -// TODO: additional/alternative encryption method, i.e DSE asymetric, see CAS robot account for reference +// TODO: additional/alternative encryption method, i.e DSE asymmetric, see CAS robot account for reference func NewBuilder(opts ...NewOpt) (*Builder, error) { b := &Builder{} for _, opt := range opts { From eacc4443a15b14632858ab95a6dada5829e7514d Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 8 Dec 2023 00:13:46 +0100 Subject: [PATCH 4/4] fix: add tests Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/biz/apitoken.go | 62 ++++---- .../internal/biz/apitoken_integration_test.go | 140 +++++++++++++----- .../internal/biz/testhelpers/wire_gen.go | 2 +- app/controlplane/internal/data/apitoken.go | 36 +++++ 4 files changed, 173 insertions(+), 67 deletions(-) diff --git a/app/controlplane/internal/biz/apitoken.go b/app/controlplane/internal/biz/apitoken.go index 30d3072c0..38400205e 100644 --- a/app/controlplane/internal/biz/apitoken.go +++ b/app/controlplane/internal/biz/apitoken.go @@ -44,22 +44,23 @@ type APIToken struct { type APITokenRepo interface { Create(ctx context.Context, description *string, expiresAt *time.Time, organizationID uuid.UUID) (*APIToken, error) + List(ctx context.Context, orgID uuid.UUID, includeRevoked bool) ([]*APIToken, error) + Revoke(ctx context.Context, orgID, ID uuid.UUID) error } type APITokenUseCase struct { - apiTokenRepo APITokenRepo - membershipRepo MembershipRepo - logger *log.Helper - jwtBuilder *apitoken.Builder + apiTokenRepo APITokenRepo + logger *log.Helper + jwtBuilder *apitoken.Builder } -func NewAPITokenUseCase(apiTokenRepo APITokenRepo, mRepo MembershipRepo, conf *conf.Auth, logger log.Logger) (*APITokenUseCase, error) { +func NewAPITokenUseCase(apiTokenRepo APITokenRepo, conf *conf.Auth, logger log.Logger) (*APITokenUseCase, error) { uc := &APITokenUseCase{ - apiTokenRepo: apiTokenRepo, - membershipRepo: mRepo, - logger: log.NewHelper(logger), + apiTokenRepo: apiTokenRepo, + logger: log.NewHelper(logger), } + // Create the JWT builder for the API token b, err := apitoken.NewBuilder( apitoken.WithIssuer(jwt.DefaultIssuer), apitoken.WithKeySecret(conf.GeneratedJwsHmacSecret), @@ -72,37 +73,17 @@ func NewAPITokenUseCase(apiTokenRepo APITokenRepo, mRepo MembershipRepo, conf *c return uc, nil } -// This is the minimum duration that a token can be created for -const minDuration = 24 * time.Hour - // expires in is a string that can be parsed by time.ParseDuration -func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expiresIn *time.Duration, userID, orgID string) (*APIToken, error) { +func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expiresIn *time.Duration, orgID string) (*APIToken, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) } - userUUID, err := uuid.Parse(userID) - if err != nil { - return nil, NewErrInvalidUUID(err) - } - - // Make sure that the organization exists and that the user is a member of it - membership, err := uc.membershipRepo.FindByOrgAndUser(ctx, orgUUID, userUUID) - if err != nil { - return nil, fmt.Errorf("failed to find memberships: %w", err) - } else if membership == nil { - return nil, NewErrNotFound("organization") - } - // If expiration is provided we store it // we also validate that it's at least 24 hours and valid string format var expiresAt *time.Time if expiresIn != nil { - if *expiresIn < minDuration { - return nil, NewErrValidation(fmt.Errorf("expiration must be at least %s", minDuration)) - } - expiresAt = new(time.Time) *expiresAt = time.Now().Add(*expiresIn) } @@ -122,3 +103,26 @@ func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expi return token, nil } + +func (uc *APITokenUseCase) List(ctx context.Context, orgID string, includeRevoked bool) ([]*APIToken, error) { + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + return uc.apiTokenRepo.List(ctx, orgUUID, includeRevoked) +} + +func (uc *APITokenUseCase) Revoke(ctx context.Context, orgID, id string) error { + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return NewErrInvalidUUID(err) + } + + uuid, err := uuid.Parse(id) + if err != nil { + return NewErrInvalidUUID(err) + } + + return uc.apiTokenRepo.Revoke(ctx, orgUUID, uuid) +} diff --git a/app/controlplane/internal/biz/apitoken_integration_test.go b/app/controlplane/internal/biz/apitoken_integration_test.go index 8257ff0f3..963f08027 100644 --- a/app/controlplane/internal/biz/apitoken_integration_test.go +++ b/app/controlplane/internal/biz/apitoken_integration_test.go @@ -33,36 +33,14 @@ import ( func (s *apiTokenTestSuite) TestCreate() { ctx := context.Background() s.T().Run("invalid org ID", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, nil, s.user.ID, "deadbeef") + token, err := s.APIToken.Create(ctx, nil, nil, "deadbeef") s.Error(err) s.True(biz.IsErrInvalidUUID(err)) s.Nil(token) }) - s.T().Run("invalid user ID", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, nil, "deadbeef", s.org.ID) - s.Error(err) - s.True(biz.IsErrInvalidUUID(err)) - s.Nil(token) - }) - - s.T().Run("user2 has no access to org", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, nil, s.user2.ID, s.org.ID) - s.Error(err) - s.True(biz.IsNotFound(err)) - s.Nil(token) - }) - - s.T().Run("expiration below threshold", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, toPtrDuration(2*time.Hour), s.user.ID, s.org.ID) - s.Error(err) - s.True(biz.IsErrValidation(err)) - s.ErrorContains(err, "expiration must be at least") - s.Nil(token) - }) - s.T().Run("happy path without expiration nor description", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, nil, nil, s.user.ID, s.org.ID) + token, err := s.APIToken.Create(ctx, nil, nil, s.org.ID) s.NoError(err) s.NotNil(token.ID) s.Equal(s.org.ID, token.OrganizationID.String()) @@ -73,7 +51,7 @@ func (s *apiTokenTestSuite) TestCreate() { }) s.T().Run("happy path with description and expiration", func(t *testing.T) { - token, err := s.APIToken.Create(ctx, toPtrS("tokenStr"), toPtrDuration(24*time.Hour), s.user.ID, s.org.ID) + token, err := s.APIToken.Create(ctx, toPtrS("tokenStr"), toPtrDuration(24*time.Hour), s.org.ID) s.NoError(err) s.Equal(s.org.ID, token.OrganizationID.String()) s.Equal("tokenStr", token.Description) @@ -82,8 +60,96 @@ func (s *apiTokenTestSuite) TestCreate() { }) } +func (s *apiTokenTestSuite) TestRevoke() { + ctx := context.Background() + + s.T().Run("invalid org ID", func(t *testing.T) { + err := s.APIToken.Revoke(ctx, "deadbeef", s.t1.ID.String()) + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + }) + + s.T().Run("invalid token ID", func(t *testing.T) { + err := s.APIToken.Revoke(ctx, s.org.ID, "deadbeef") + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + }) + + s.T().Run("token not found in org", func(t *testing.T) { + err := s.APIToken.Revoke(ctx, s.org.ID, s.t3.ID.String()) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) + + s.T().Run("token can be revoked once", func(t *testing.T) { + err := s.APIToken.Revoke(ctx, s.org.ID, s.t1.ID.String()) + s.NoError(err) + tokens, err := s.APIToken.List(ctx, s.org.ID, true) + s.NoError(err) + s.Equal(s.t1.ID, tokens[0].ID) + // It's revoked + s.NotNil(tokens[0].RevokedAt) + + // Can't be revoked twice + err = s.APIToken.Revoke(ctx, s.org.ID, s.t1.ID.String()) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) +} + +func (s *apiTokenTestSuite) TestList() { + ctx := context.Background() + s.T().Run("invalid org ID", func(t *testing.T) { + tokens, err := s.APIToken.List(ctx, "deadbeef", false) + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + s.Nil(tokens) + }) + + s.T().Run("returns empty list", func(t *testing.T) { + emptyOrg, err := s.Organization.Create(ctx, "org1") + require.NoError(s.T(), err) + tokens, err := s.APIToken.List(ctx, emptyOrg.ID, false) + s.NoError(err) + s.Len(tokens, 0) + }) + + s.T().Run("returns the tokens for that org", func(t *testing.T) { + var err error + tokens, err := s.APIToken.List(ctx, s.org.ID, false) + s.NoError(err) + require.Len(s.T(), tokens, 2) + s.Equal(s.t1.ID, tokens[0].ID) + s.Equal(s.t2.ID, tokens[1].ID) + + tokens, err = s.APIToken.List(ctx, s.org2.ID, false) + s.NoError(err) + require.Len(s.T(), tokens, 1) + s.Equal(s.t3.ID, tokens[0].ID) + }) + + s.T().Run("doesn't return revoked by default", func(t *testing.T) { + // revoke one token + err := s.APIToken.Revoke(ctx, s.org.ID, s.t1.ID.String()) + require.NoError(s.T(), err) + tokens, err := s.APIToken.List(ctx, s.org.ID, false) + s.NoError(err) + require.Len(s.T(), tokens, 1) + s.Equal(s.t2.ID, tokens[0].ID) + }) + + s.T().Run("doesn't return revoked unless requested", func(t *testing.T) { + // revoke one token + tokens, err := s.APIToken.List(ctx, s.org.ID, true) + s.NoError(err) + require.Len(s.T(), tokens, 2) + s.Equal(s.t1.ID, tokens[0].ID) + s.Equal(s.t2.ID, tokens[1].ID) + }) +} + func (s *apiTokenTestSuite) TestGeneratedJWT() { - token, err := s.APIToken.Create(context.Background(), nil, toPtrDuration(24*time.Hour), s.user.ID, s.org.ID) + token, err := s.APIToken.Create(context.Background(), nil, toPtrDuration(24*time.Hour), s.org.ID) s.NoError(err) require.NotNil(s.T(), token) @@ -109,8 +175,8 @@ func TestAPITokenUseCase(t *testing.T) { // Utility struct to hold the test suite type apiTokenTestSuite struct { testhelpers.UseCasesEachTestSuite - org *biz.Organization - user, user2 *biz.User + org, org2 *biz.Organization + t1, t2, t3 *biz.APIToken } func (s *apiTokenTestSuite) SetupTest() { @@ -122,15 +188,15 @@ func (s *apiTokenTestSuite) SetupTest() { s.TestingUseCases = testhelpers.NewTestingUseCases(t) s.org, err = s.Organization.Create(ctx, "org1") assert.NoError(err) - - // Create User 1 - s.user, err = s.User.FindOrCreateByEmail(ctx, "user-1@test.com") - assert.NoError(err) - // Attach org 1 - _, err = s.Membership.Create(ctx, s.org.ID, s.user.ID, true) + s.org2, err = s.Organization.Create(ctx, "org2") assert.NoError(err) - // Create user 2 with no orgs - s.user2, err = s.User.FindOrCreateByEmail(ctx, "user-2@test.com") - assert.NoError(err) + // Create 2 tokens for org 1 + s.t1, err = s.APIToken.Create(ctx, nil, nil, s.org.ID) + require.NoError(s.T(), err) + s.t2, err = s.APIToken.Create(ctx, nil, nil, s.org.ID) + require.NoError(s.T(), err) + // and 1 token for org 2 + s.t3, err = s.APIToken.Create(ctx, nil, nil, s.org2.ID) + require.NoError(s.T(), err) } diff --git a/app/controlplane/internal/biz/testhelpers/wire_gen.go b/app/controlplane/internal/biz/testhelpers/wire_gen.go index 71abb944c..32fe98544 100644 --- a/app/controlplane/internal/biz/testhelpers/wire_gen.go +++ b/app/controlplane/internal/biz/testhelpers/wire_gen.go @@ -83,7 +83,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r return nil, nil, err } apiTokenRepo := data.NewAPITokenRepo(dataData, logger) - apiTokenUseCase, err := biz.NewAPITokenUseCase(apiTokenRepo, membershipRepo, auth, logger) + apiTokenUseCase, err := biz.NewAPITokenUseCase(apiTokenRepo, auth, logger) if err != nil { cleanup() return nil, nil, err diff --git a/app/controlplane/internal/data/apitoken.go b/app/controlplane/internal/data/apitoken.go index fcf333f11..fc7482de5 100644 --- a/app/controlplane/internal/data/apitoken.go +++ b/app/controlplane/internal/data/apitoken.go @@ -22,6 +22,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/data/ent/apitoken" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" ) @@ -52,6 +53,41 @@ func (r *APITokenRepo) Create(ctx context.Context, description *string, expiresA return entAPITokenToBiz(token), nil } +func (r *APITokenRepo) List(ctx context.Context, orgID uuid.UUID, includeRevoked bool) ([]*biz.APIToken, error) { + query := r.data.db.APIToken.Query().Where(apitoken.OrganizationIDEQ(orgID)) + if !includeRevoked { + query = query.Where(apitoken.RevokedAtIsNil()) + } + + tokens, err := query.Order(ent.Asc(apitoken.FieldCreatedAt)).All(ctx) + if err != nil { + return nil, err + } + + result := make([]*biz.APIToken, 0, len(tokens)) + for _, t := range tokens { + result = append(result, entAPITokenToBiz(t)) + } + + return result, nil +} + +func (r *APITokenRepo) Revoke(ctx context.Context, orgID, id uuid.UUID) error { + // Update a token with id = id that has not been revoked yet and its orgID = orgID + err := r.data.db.APIToken.UpdateOneID(id). + Where(apitoken.OrganizationIDEQ(orgID), apitoken.RevokedAtIsNil()). + SetRevokedAt(time.Now()).Exec(ctx) + if err != nil { + if ent.IsNotFound(err) { + return biz.NewErrNotFound("API token") + } + + return fmt.Errorf("revoking APIToken: %w", err) + } + + return nil +} + func entAPITokenToBiz(t *ent.APIToken) *biz.APIToken { return &biz.APIToken{ ID: t.ID,